The choose, explore, undo pattern
def search(state):
if complete(state):
record(state.copy())
return
for choice in candidates(state):
if valid(choice, state):
apply(choice, state)
search(state)
undo(choice, state)
The copy matters when recording answers: if the mutable state itself is stored, later undo steps can change the saved solution.
Worked example: find a path through a grid
From the start cell, choose an unvisited open neighbour and continue. If the route reaches a wall or a cell with no available neighbour, return to the previous cell and try another direction. Rejecting a route at the first dead end avoids exploring paths that cannot reach the goal.
Use Step in the visualizer to follow one branch. When it reaches a dead end, watch it unmark cells and return to the previous choice. The undo restores the shared grid for another route.
Cost and when to use it
Backtracking can still take exponential time in the worst case: if few partial candidates can be rejected, it approaches exhaustive search. Its actual cost depends on the branching factor, search depth, and how early constraints prune branches. It fits puzzles, scheduling, combinations, and other problems where a partial answer can be tested cheaply.
Order choices to expose contradictions early, and maintain constraint state so validity checks are inexpensive. For large search spaces, stronger methods such as constraint propagation, dynamic programming, or specialized solvers may be needed.
Common mistakes with Backtracking Search
- Forgetting to undo state: later branches inherit choices from earlier ones.
- Checking constraints only at the end: invalid branches waste work.
- Saving a reference instead of a copy: recorded solutions mutate during backtracking.
- Assuming pruning guarantees speed: worst-case search can remain exponential.