Backtracking
Build a solution incrementally, abandoning a branch the moment it can't possibly lead to a valid answer. Choose. Explore. Unchoose. One template, every combinatorial search problem.
Brute force generates every candidate then filters. Backtracking abandons a branch the instant it can't succeed — pruning the exponential tree dramatically.
The key: backtracking explores a decision tree. At each node, you make a choice, recurse into the subtree, then undo the choice to try the next option. If a partial solution already violates a constraint, you prune the entire subtree — potentially saving millions of recursive calls.
The Universal Template
Memorise this. Every backtracking problem is a variation.
Add the current candidate to the path. Update any state that tracks what's been used (visited set, running sum, etc.).
path.append(choice)
mark_used(choice)Recurse with the updated path. The recursive call explores all possibilities from the current state. Return when the base case is hit.
backtrack(
path, remaining
)Undo the choice. Remove from path, unmark as used. The state must be identical to before the CHOOSE step.
path.pop()
unmark_used(choice)Prune before recursing: check constraints after CHOOSE, before EXPLORE
State symmetry: after backtrack() returns, state must equal state before backtrack() was called
The 3-Step Framework
Three decisions determine your backtracking solution.
This is your base case. For combination sum: path sums to target. For word search: index equals word length. For sudoku: no empty cells. When you hit the base case, record path.copy() and return.
# Combination Sum: remaining == 0
if remaining == 0:
result.append(path.copy())
return
# Word Search: matched all chars
if index == len(word):
return TrueThis is where backtracking beats brute force. Prune as early as possible: if current sum exceeds target, stop. If current cell is out of bounds or already visited, stop. If current digit fails sudoku constraints, skip. Every pruned node = entire subtree avoided.
# Combination Sum: sorted → break early
if candidate > remaining:
break # all further candidates also too big
# Word Search: multiple checks in one condition
if (r<0 or r>=rows or c<0 or c>=cols
or board[r][c]=='#'
or board[r][c]!=word[idx]):
return FalseWhat are the candidates at each step? For combination sum: all candidates from start index onward (sorted, allows reuse). For word search: 4 adjacent cells. For sudoku: digits 1–9. For substrings: all possible ending positions. The search space defines your branching factor.
# Combinations with reuse
for i in range(start, len(candidates)):
backtrack(i, ...) # i not i+1
# Without reuse (permutations/subsets)
for i in range(start, len(nums)):
backtrack(i+1, ...) # i+1Pattern Recognition
'Find ALL …' or 'does a solution EXIST' → backtracking.