Une fonction récursive s'appelle elle-même avec une version plus petite du problème jusqu'à atteindre un cas de base qui arrête la chaîne.
Toute récursion a besoin de deux choses :
def factorial(n):
if n <= 1: # base case
return 1
return n * factorial(n - 1) # recursive case
Chaque appel récursif empile un cadre (frame) sur la pile d'appels. Quand le cas de base retourne, les cadres se dépilent en ordre inverse.
factorial(4)
→ factorial(3)
→ factorial(2)
→ factorial(1) → returns 1
← returns 2
← returns 6
← returns 24
Sans cas de base, la pile déborde. La limite de récursion par défaut de Python est de 1 000 frames.
Le Fibonacci récursif naïf est en O(2ⁿ) car il recalcule les mêmes sous-problèmes. La mémoïsation corrige cela en O(n) en temps et en espace.
from functools import lru_cache
@lru_cache(maxsize=None)
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
Le retour sur trace (backtracking) est la récursivité avec une astuce : on fait un choix, on recurse, puis on annule le choix avant d'essayer l'option suivante. Comme explorer un labyrinthe - avancer, atteindre un cul-de-sac, revenir en arrière, essayer un autre chemin.
def backtrack(state, choices):
if is_solution(state):
result.append(state.copy())
return
for choice in choices:
if is_valid(choice):
state.add(choice) # choose
backtrack(state, ...) # explore
state.remove(choice) # un-choose
Ce patron en trois étapes - - est la colonne vertébrale de presque tout problème de backtracking.
Sign in to join the discussion
Dans le template de backtracking, pourquoi annule-t-on le choix après l'appel récursif ?
Générer tous les ordres de [1, 2, 3]. À chaque niveau, choisir un nombre non utilisé, recurser, puis le retirer.
[]
/ | \
[1] [2] [3]
/ \ ...
[1,2] [1,3]
| |
[1,2,3] [1,3,2] ... (6 total)
Il y a n! permutations, donc la complexité est O(n × n!).
Combinaisons (n parmi k) : pour chaque élément, l'inclure ou le sauter, en ne récursant que vers l'avant pour éviter les doublons.
Sous-ensembles : même idée sans contrainte de taille - chaque nœud de l'arbre de récursion est un sous-ensemble valide. Il y a 2ⁿ sous-ensembles.
Placer N reines sur un échiquier N×N de sorte qu'aucune ne s'attaque. Pour N=4 :
. Q . .
. . . Q
Q . . .
. . Q .
Stratégie : placer une reine par ligne. Pour chaque ligne, essayer chaque colonne. Vérifier les conflits de colonne, diagonale et anti-diagonale. Si c'est sûr, placer et recurser. Sinon, backtracker.
On suit les conflits avec trois ensembles : cols, diag (row − col) et anti_diag (row + col).
Dans le problème des N-Queens, comment vérifie-t-on efficacement les conflits diagonaux ?
Trouver une cellule vide, essayer les chiffres 1–9. Pour chaque chiffre, vérifier les contraintes de ligne, colonne et carré 3×3. Si valide, placer et recurser. Si aucun chiffre ne marche, annuler et backtracker.
Étant donné un plateau 2D de lettres et un mot cible, démarrer depuis chaque cellule et DFS dans quatre directions, en faisant correspondre un caractère à la fois. Marquer les cellules visitées pendant la récursion et démarquer au backtrack.
Le coût dépend du facteur de branchement (choix par étape) et de la profondeur. Pour b branches et d profondeur : O(bᵈ).
| Problème | Branchement | Profondeur | Complexité | |----------|-------------|------------|------------| | Permutations | n, n−1, … | n | O(n!) | | Sous-ensembles | 2 | n | O(2ⁿ) | | N-Queens | ~n | n | O(n!) pire cas |
L'élagage (pruning) consiste à ignorer les branches vouées à l'échec :
Un bon élagage peut rendre une recherche impraticable rapide.
Que fait l'élagage dans un algorithme de backtracking ?