Een recursieve functie roept zichzelf aan met een kleinere versie van het probleem, totdat het een basisgeval bereikt dat de keten stopt.
Elke recursie heeft twee dingen nodig:
def factorial(n):
if n <= 1: # basisgeval
return 1
return n * factorial(n - 1) # recursief geval
Elke recursieve aanroep pusht een frame op de call stack. Wanneer het basisgeval retourneert, worden frames in omgekeerde volgorde ge-popt.
factorial(4)
→ factorial(3)
→ factorial(2)
→ factorial(1) → geeft 1 terug
← geeft 2 terug
← geeft 6 terug
← geeft 24 terug
Als er geen basisgeval is, loopt de stack over. Python's standaard recursielimiet is 1.000 frames.
De naïeve recursieve Fibonacci is O(2ⁿ) omdat het dezelfde deelproblemen herberekent. Memoisatie lost dit op in O(n) tijd en ruimte.
from functools import lru_cache
@lru_cache(maxsize=None)
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
Backtracking is recursie met een twist: je maakt een keuze, recurseert, en maakt de keuze ongedaan voordat je de volgende optie probeert. Zie het als het verkennen van een doolhof - loop vooruit, loop vast, loop terug, probeer het volgende pad.
def backtrack(state, choices):
if is_solution(state):
result.append(state.copy())
return
for choice in choices:
if is_valid(choice):
state.add(choice) # kies
backtrack(state, ...) # verken
state.remove(choice) # maak ongedaan
Dit drieledige patroon - kiezen, verkennen, ongedaan maken - is de ruggengraat van vrijwel elk backtracking-probleem.
Sign in to join the discussion
Waarom maken we de keuze ongedaan na de recursieve aanroep in het backtracking-sjabloon?
Genereer alle volgordes van [1, 2, 3]. Op elk niveau kies een ongebruikt getal, recurseer, en maak dan de keuze ongedaan.
[]
/ | \
[1] [2] [3]
/ \ ...
[1,2] [1,3]
| |
[1,2,3] [1,3,2] ... (6 totaal)
Er zijn n! permutaties, dus de tijdcomplexiteit is O(n × n!).
Combinaties (n boven k): bij elk element neem je het op of sla je het over, maar recurseer alleen vooruit om duplicaten te vermijden.
Deelverzamelingen: zelfde idee zonder beperking - elk knooppunt in de recursieboom is een geldige deelverzameling. Er zijn 2ⁿ deelverzamelingen.
Plaats N koninginnen op een N×N-bord zodat geen enkele de ander aanvalt. Voor N=4:
. Q . .
. . . Q
Q . . .
. . Q .
Strategie: plaats één koningin per rij. Probeer voor elke rij elke kolom. Controleer kolom-, hoofddiagonaal- en antidiagonaalconflicten. Als veilig, plaats en recurseer naar de volgende rij. Als vast, backtrack.
Houd conflicten bij met drie sets: cols, diag (rij − kolom) en anti_diag (rij + kolom).
Hoe controleren we diagonaalconflicten efficiënt bij het N-Queens-probleem?
Vind een lege cel, probeer cijfers 1–9. Controleer voor elk cijfer rij-, kolom- en 3×3-vakbeperkingen. Als geldig, plaats en recurseer. Als geen cijfer werkt, maak ongedaan en backtrack. De snoeiing door beperkingen maakt het haalbaar ondanks de enorme zoekruimte.
Gegeven een 2D-bord met letters en een doelwoord: start vanuit elke cel en DFS in vier richtingen, één karakter per keer matchend. Markeer cellen als bezocht tijdens recursie en demarkeer bij backtrack om andere paden mogelijk te maken.
De kosten hangen af van de vertakkingsfactor (keuzes per stap) en diepte (stappen tot een oplossing). Voor b vertakkingen en d diepte: O(bᵈ).
| Probleem | Vertakking | Diepte | Complexiteit | |----------|------------|--------|-------------| | Permutaties | n, n−1, … | n | O(n!) | | Deelverzamelingen | 2 | n | O(2ⁿ) | | N-Queens | ~n | n | O(n!) slechtste geval |
Snoeiing (pruning) betekent het overslaan van takken die je weet dat ze falen. Voorbeelden:
Goede snoeiing kan een onpraktische zoekopdracht omzetten in een snelle.
Wat doet snoeiing (pruning) in een backtracking-algoritme?