Backtracking & Recursive Search
Backtracking is what brute force looks like when it grows up. The naive approach to "find all valid arrangements" builds every possible arrangement and throws away the bad ones at the end. Backtracking builds arrangements one choice at a time, checks as it goes, and the instant a partial choice cannot possibly lead anywhere, it abandons that whole branch and steps back. It is a disciplined walk through a tree of decisions: make a choice, explore what follows, then undo the choice and try the next one.
That undo is the heartbeat of the pattern, and the single most common place people get it wrong. You mutate some shared state to record a choice, recurse, and then you must faithfully un-mutate it on the way back up, or every later branch inherits a decision it never made. Choose, explore, un-choose. Miss the last step and the whole search quietly corrupts.
Choose, explore, un-choose
Generating every permutation of a list is the skeleton in its purest form. Add an element to the current path, recurse on what remains, then pop it back off before trying the next.
def permutations(nums):
result = []
def backtrack(path, remaining):
if not remaining: # a complete arrangement
result.append(path[:]) # copy, the path keeps mutating
return
for i in range(len(remaining)):
path.append(remaining[i]) # choose
backtrack(path, remaining[:i] + remaining[i+1:]) # explore
path.pop() # un-choose
backtrack([], nums)
return result
For [1, 2, 3] it yields all six orderings. Notice the path[:] copy when recording a result, because path itself is a single list mutated throughout the search, and the path.pop() that restores it. That pop is the backtrack. Without it, the second branch would start from a dirty path and the output would be nonsense.
In the wild: solving Sudoku
The same skeleton, plus one crucial ingredient, solves real constraint problems. A Sudoku solver walks empty cells, tries each digit, recurses, and backtracks when it hits a wall. The ingredient that makes it fast rather than astronomically slow is pruning: before recursing on a digit, check that it does not already violate a row, column, or box. That check kills doomed branches before they explode.
def solve(board): # board is 9x9, 0 means empty
def ok(r, c, v):
for i in range(9):
if board[r][i] == v or board[i][c] == v:
return False
if board[3*(r//3) + i//3][3*(c//3) + i%3] == v:
return False
return True
def backtrack():
for r in range(9):
for c in range(9):
if board[r][c] == 0:
for v in range(1, 10):
if ok(r, c, v):
board[r][c] = v # choose
if backtrack(): # explore
return True
board[r][c] = 0 # un-choose
return False # no digit fits, dead end
return True # no empty cell left, solved
backtrack()
return board
It is the permutation skeleton in disguise: choose a value for a cell, explore, and undo it (board[r][c] = 0) when the branch fails. What turns an impossible 9^81 search into something that finishes instantly is the ok check pruning the tree. That is the real lesson of backtracking, and the same shape solves N-Queens, word search in a grid, and the constraint solvers inside schedulers and dependency resolvers, where a package manager tries version combinations and backs out of conflicts.
The trigger
The problem asks for all solutions, or any valid configuration, of something built from a sequence of choices under constraints. Permutations, combinations, subsets, board-filling, path-finding through a maze of decisions. If you can describe it as "make a choice, then solve the rest," and choices can fail, backtracking is the frame.
Where it shows up
- Enumeration: all permutations, combinations, subsets, or parenthesizations.
- Constraint satisfaction: Sudoku, N-Queens, graph coloring, cryptarithms.
- Path and parsing search: word search in a grid, regular-expression matching, splitting a string into valid pieces.
Where it bites
Two failures dominate. Forgetting to undo state, the missing pop or reset, so branches leak into each other. And skipping the pruning, which leaves a correct but hopelessly slow search: the difference between a Sudoku solver that returns in a blink and one that never finishes is entirely the early constraint check. Deep recursion can also overflow the stack on large instances.
When it is the wrong tool
If the branches revisit the same subproblem, pure backtracking recomputes it exponentially, and you should add memoization, which is exactly the door into Dynamic Programming. If a proven Greedy rule or a direct construction yields a valid answer, enumerating the whole tree to find one is wasteful. And if the space is enormous with no structure to prune on, exact backtracking may simply be intractable, and heuristics, randomized search, or approximation become the realistic path.
Its neighbors
Backtracking is really Depth-First Search on an implicit tree of choices, so it shares everything with graph traversal. It becomes Dynamic Programming the moment you memoize overlapping subproblems, and it leans on plain recursion as its control structure. Pruning, its make-or-break feature, is a Greedy-flavored judgment about which branches are worth entering.
References
- Introduction to Algorithms (CLRS), 4th ed., Cormen, Leiserson, Rivest, Stein, 2022
- The Algorithm Design Manual, 3rd ed., Steven Skiena, 2020