Backtracking
DFS over a tree of choices — the choose/explore/un-choose template, the subsets/permutations/combinations recipes, pruning, and constraint solving.
The idea
Backtracking is DFS over a tree of choices: pick an option, recurse, then undo it and try the next. It enumerates subsets, permutations, combinations, and solves constraint problems (N-Queens, Sudoku, word search). The cost is the number of nodes in the choice tree — exponential — so it’s for small inputs or problems where you prune hard.
The template
def subsets(nums):
res, path = [], []
def dfs(start):
res.append(path[:]) # record (a copy of) the current partial answer
for i in range(start, len(nums)):
path.append(nums[i]) # choose
dfs(i + 1) # explore
path.pop() # un-choose (backtrack)
dfs(0)
return res
Three lines — choose, explore, un-choose — are the entire pattern. Always append a copy
(path[:]) when recording, since path keeps mutating.
The three canonical shapes
The recursion structure differs by what you’re enumerating:
- Subsets — at each index, include or skip; pass
start = i + 1to avoid reusing earlier elements (order doesn’t matter). - Combinations (choose k, or sum to target) — like subsets but with a target/size and a
startindex so[1,2]and[2,1]aren’t both produced. - Permutations — order matters, so iterate all unused elements each level (track a
used[]set, nostart):
def permutations(nums):
res, path, used = [], [], [False] * len(nums)
def dfs():
if len(path) == len(nums):
res.append(path[:]); return
for i in range(len(nums)):
if used[i]: continue
used[i] = True; path.append(nums[i])
dfs()
path.pop(); used[i] = False
dfs()
return res
Pruning (what makes it tractable)
The search space is exponential — cut branches early:
- Skip duplicates — sort first, then
if i > start and nums[i] == nums[i-1]: continue(subsets/combinations with duplicates), or skip a duplicate at the same tree level for permutations. - Bound checks — stop the branch the moment it can’t succeed (remaining target < 0, partial already invalid).
- Constraint propagation — for N-Queens/Sudoku, track attacked columns/diagonals (sets) so invalid placements are O(1) to reject.
- Visited marks — mark/unmark grid cells in place for word search (and restore on backtrack).
Good pruning often turns “times out” into “instant.”
Constraint-satisfaction problems
N-Queens, Sudoku, and graph coloring place items subject to rules. The pattern: try each value for the next slot, check feasibility (ideally incrementally with sets/bitmasks), recurse, and undo. Strong pruning (forward-checking, most-constrained-variable ordering) is the difference between fast and hopeless.
Complexity
Roughly O(nodes in the choice tree × work per node): subsets O(2^n), permutations O(n!),
combinations O(C(n,k)). State this and the input bound — it signals you know it’s exponential by
nature.
When to use it
The answer is “all valid arrangements,” “does any valid arrangement exist,” or “the best arrangement” and greedy/DP don’t apply: Subsets, Permutations, Combination Sum, Palindrome Partitioning, Generate Parentheses, N-Queens, Sudoku Solver, Word Search.
Pitfalls
- Recording a reference instead of a copy — append
path[:]. - Forgetting to undo the choice (state leaks into sibling branches).
- Duplicate results — sort + skip-equal, or use a per-level seen set.
- No pruning — exponential blowup; always look for a feasibility cut.
If the problem asks for a count or an optimum over overlapping subproblems rather than the arrangements themselves, it’s likely DP — see the DP primers.