Backtracking
Try it, undo it, try the next thing.
Backtracking is structured brute force: explore every candidate solution by building it one choice at a time, and the moment a partial build can't possibly work, rip the last choice out and try another. One template — choose, explore, un-choose — generates subsets, permutations, sudoku solvers, and N-Queens. Learn the skeleton once and every "generate all…" problem becomes a fill-in-the-blanks exercise.
01When the constraints scream backtracking
Look at the input size before you look at anything else. Backtracking explores an exponential space — 2^n subsets, n! permutations — so it's only viable when n is tiny.
| Constraint | What it whispers |
|---|---|
n <= 10^5 | O(n) or O(n log n) — hash maps, sorting, two pointers |
n <= 1000 | O(n²) is fine |
n <= ~20 | exponential is expected — backtracking / bitmask |
2^20 ≈ 10^6 — comfortably fast. 2^40 is a trillion — dead. So when you see nums.length <= 20, or "return all subsets / permutations / combinations / valid boards", the interviewer is telling you to enumerate exhaustively. The skill being tested isn't cleverness; it's whether you can enumerate without missing anything and without repeating anything.
Tell: the word "all" in the output ("return all…") plus a small n is the loudest backtracking signal in interviews.
02The decision tree: every solution is a root-to-leaf path
Picture the search as a tree. The root is the empty partial solution. Each edge is one choice (include this number, place a queen in this column, cut the string here). Each leaf is a complete candidate.
nums = [1, 2] []
include 1 / \ skip 1
[1] []
include 2 / \ / \ include 2
[1,2] [1] [2] []
Backtracking is just depth-first search on this tree — except the tree is never built in memory. You walk it: descend by making a choice, and when you return, undo the choice so the shared path list is clean for the next branch.
Two questions design the whole tree:
- What are the choices at this node? (which elements / positions / cuts are still legal)
- When is a node a leaf? (the partial solution is complete — or provably hopeless)
Answer those two and the code writes itself.
Enter the root with an empty path — record {} first; the empty set is a subset too.
def backtrack(start, path):record(path[:]) # every node is a subsetfor i in range(start, len(nums)):path.append(nums[i]) # CHOOSEbacktrack(i + 1, path) # EXPLOREpath.pop() # UN-CHOOSE# 2^3 = 8 subsets recorded
03The universal template: choose → explore → un-choose
Every backtracking solution you will ever write is this function wearing a different costume:
def solve(problem):
results = []
path = [] # the partial solution, shared & mutated
def backtrack(state):
if is_complete(state): # leaf: record a snapshot
results.append(path[:]) # copy! path keeps mutating after this
return
for choice in legal_choices(state):
path.append(choice) # 1. CHOOSE
backtrack(next_state) # 2. EXPLORE
path.pop() # 3. UN-CHOOSE — restore exactly
backtrack(initial_state)
return results
Three rules that prevent 90% of bugs:
- Snapshot at the leaf.
results.append(path)stores a reference to a list you're about to mutate — every result ends up empty. Alwayspath[:]orlist(path). - Undo must mirror do. Whatever
append/mark/set you did before recursing, reverse it after — exactly once, unconditionally. - Pass the frontier, not the history. A
startindex (combinations) or ausedset (permutations) tells each node which choices remain.startenforces "only look rightwards," which is what kills duplicate orderings of the same combination.
04Pruning: stop digging when the hole is hopeless
Raw backtracking visits every node. Pruning chops entire subtrees the moment a partial solution becomes impossible — and it's the difference between "times out" and "instant."
def backtrack(start, remaining):
if remaining == 0:
results.append(path[:]); return
for i in range(start, len(candidates)):
if candidates[i] > remaining: # sorted input ⇒ everything after
break # is bigger too: prune the rest
path.append(candidates[i])
backtrack(i, remaining - candidates[i])
path.pop()
The classic pruning moves:
- Sort first, then
breakwhen a candidate overshoots — every later candidate overshoots too. - Check constraints on the way down, not at the leaf. In N-Queens, test column and diagonal conflicts before placing — don't build a full board and validate it at the end.
- Skip duplicate branches: with sorted input,
if i > start and nums[i] == nums[i-1]: continueensures only the first copy of a value starts a branch at each tree level. Same subtree, explored once.
Say the word "prune" in the interview. Naming the optimization is worth as much as coding it.
05Reading a problem into the template
The template has four slots. Filling them is solving the problem:
| Problem | Choice at each node | Leaf condition | Prune |
|---|---|---|---|
| Subsets | include/skip nums[i] | considered every index | — (every node is also a result) |
| Permutations | any unused element | len(path) == n | skip used elements |
| Combination sum | any candidate from start | remaining hits 0 | candidate > remaining |
| Palindrome cuts | next cut position | consumed whole string | prefix isn't a palindrome |
| N-Queens | column for current row | placed n queens | column/diagonal under attack |
Notice the complexity is the size of the output times the cost per leaf — O(n · 2^n) for subsets, O(n · n!) for permutations. You can't beat the output size; when asked "can you do better?", that's the answer.
Interview script: state the choices, state the leaf, write the skeleton, then ask yourself "what branches can I prove dead early?" — out loud.
✦Prove it — checkpoint quiz
A problem says nums.length <= 18 and asks for all valid combinations. What's the intended approach?
Inside backtrack(), why is results.append(path) a bug while results.append(path[:]) is correct?
In combination problems, what does passing a start index to the recursion accomplish?
You sort candidates ascending in combination-sum. Why does that unlock `break` (not just `continue`) when candidates[i] > remaining?
⚒Now forge it
Reading is scaffolding. These drills are the building. Easiest first: