{} Coding Interviews · ch.18 · backtracking
🧩 Part IV · Graphs & Exhaustive Search · chapter 18 / 24

Try everything —
with manners

Backtracking is the polite brute force: make a choice, explore it to the end, then undo it as if you were never there. The recursion tree visits every candidate — and pruning teaches it to slam the door on doomed ones early.

Every pattern so far exploited some structure — sortedness, monotonicity, a hierarchy, a frontier. This chapter is for when there is none: the problem says all of them, and the only honest move is to try everything. Backtracking is how you try everything without drowning — one shared, mutable partial answer, extended and retracted, walking an exponential tree in linear memory.

It's the last stop in Part IV for a reason: backtracking is just Chapter 11's DFS pointed at an imaginary tree — the tree of choices instead of a tree of nodes. Learn one skeleton here and you've solved Subsets, Permutations, Combination Sum, Word Search, Palindrome Partitioning, and N-Queens. They differ in exactly two lines: what counts as a choice, and what you refuse to try.

1Try everything, tidy up after — backtracking

Suppose the problem asks for all subsets, all permutations, every valid arrangement. There's no shortcut hiding in the statement — the answer itself is exponentially large, so any algorithm must at least touch every answer. The naive way to do that is to materialize candidates: build every string of n choices, then filter the legal ones. That drowns fast, because most of those candidates were doomed at choice two.

Backtracking builds candidates incrementally instead. Keep one partial answer — a growing path. At each step, enumerate the choices legal right now, apply one, recurse, and then — the signature move — undo it and try the next. The undo is what makes it brute force with manners: every branch inherits a clean state, and the whole exponential search runs in O(depth) extra memory — one path, reused a billion times.

And because a doomed partial answer is detected at depth two instead of depth twenty, whole subtrees vanish before they're born. That's pruning, and it's Section 5's whole job.

🎯
The tell: "all possible…", "generate every…", "count the ways to arrange…" — plus a small n (≤ 20 or so) in the constraints. Chapter 3 called that constraint the interviewer whispering "exponential is fine". The skeleton never changes; only the choice list and the prune do.

2The tree you never build — the recursion tree IS the search space

Picture every partial answer as a node: the root is the empty path, each edge is one choice, each leaf is a complete candidate. For Subsets Medium on [1,2,3], every element gets a binary verdict — in or out — so the tree is a full binary tree of depth 3 with 2³ = 8 leaves, one per subset.

Here's the part that makes backtracking cheap to write: you never build this tree. It exists only as the trace of your recursion. The call stack holds exactly one root-to-current-node path at any moment; "choose" walks down an edge, "un-choose" walks back up. Chapter 11 said pre-order and post-order are about when you look at a node — choose is your pre-order moment, un-choose is your post-order moment. Same DFS, imaginary tree.

Walk it live. Solid green edges take an element, dashed grey edges skip it; watch the ochre path snake down, retreat, and snake down again — and watch the eight subsets drop out of the leaves in DFS order.

Interactive · the decision-tree explorer Subsets of [1,2,3] — solid edge = take, dashed = skip
path: ∅
nodes visited
0 / 15
subsets collected
0 / 8
un-chooses (pops)
0
💡
Tree size is your complexity forecast. Binary in/out choices → 2ⁿ leaves (subsets). Choosing an order → n! leaves (permutations). The work is leaves × cost per leaf, usually an O(n) copy — so Subsets is O(n · 2ⁿ), "every subset, copied once". If that number scares you, re-read the constraints: it was probably n ≤ 16.

3Choose, explore, un-choose — the three-line liturgy

Strip every backtracking solution to its spine and you find the same three lines inside a loop over choices:

  • Choose. Mutate shared state: path.append(x), mark a cell visited, place a queen.
  • Explore. Recurse. The child sees the world with your choice applied.
  • Un-choose. Undo exactly what choose did: path.pop(), unmark, remove the queen. The world is restored for the next iteration of the loop.

Why mutate-and-undo instead of passing fresh copies down? Copies are correct — and in a pinch they're a fine way to get unstuck — but copying an O(n) path at every one of 2ⁿ nodes buys you an extra factor of n and a garbage-collector workout. The undo version pays O(1) per edge. More importantly, the undo is the backtrack: returning from the recursion physically re-enters the parent's world. When people say "then we backtrack", this pair of lines is the thing they're saying.

⚠️
The two classic bugs, both beloved by interviewers: (1) an asymmetric undo — you pop() the path but forget to unmark used[i], and later branches inherit a haunted state; every mutation in choose needs its mirror in un-choose. (2) collecting res.append(path) without a copy — in Python that stores a reference to the one shared list, and at the end your answer is n copies of the empty path. path[:] or bust.

4Three costumes, one skeleton — subsets, permutations, combinations

The three "generate all…" staples are the same skeleton wearing different choice lists:

  • Subsets Medium — choices are "elements from index start onward"; recurse with i + 1 so nothing is reused; every node of the tree is an answer, so collect unconditionally on entry. 2ⁿ results.
  • Combinations — identical to subsets (same start-index trick), but only leaves of size k are collected. Combination Sum Medium is this with a twist: recurse with i, not i + 1, because reusing a candidate is allowed.
  • Permutations Medium — order matters, so the choice list is "every element not yet used" (a used[] array is choose/un-chosen alongside the path); collect only at full length. n! results.

The start index versus the used array is the entire difference between "order doesn't matter" and "order matters" — one integer versus one boolean array. Generate Parentheses Medium is the same skeleton again with exactly two choices per node: add ( if any remain, add ) if it wouldn't overrun.

Duplicate inputs get one extra move: sort first, then skip equal neighbors at the same depthif i > start and nums[i] == nums[i-1]: continue. That single line deduplicates Subsets II and Combination Sum II without a set of seen-answers in sight. (Chapter 5 played the same skip-duplicates card in 3Sum — sorted input keeps paying rent.)

5Refuse doomed branches — pruning

Backtracking's superpower isn't visiting the tree — it's refusing most of it. A prune is a test you run before recursing: if this choice provably cannot lead to any answer, don't take the edge at all. The whole subtree below it — possibly thousands of nodes — evaporates for the cost of one comparison.

Combination Sum Medium makes the arithmetic visible. Target 8 from candidates [2,3,5], reuse allowed. Carry remaining down the tree. Without pruning, you descend into hopeless branches and discover the overshoot after arriving (remaining goes negative — a dead end you paid full price to visit). With pruning, one glance — candidate > remaining — refuses the branch at the door. And because the candidates are sorted, that's a break, not a continue: if 3 is too big, 5 certainly is.

Run it both ways and read the node counters. Same answers, same skeleton — different bill.

Interactive · the pruning shears Combination Sum: [2,3,5] → target 8, reuse allowed
nodes visited
0
dead ends entered
0
solutions found
0 / 3
pruning saves
💡
Pruning rarely changes the worst-case big-O — the tree is still exponential on adversarial input. What it changes is the constant, and at 2ⁿ the constant is the whole game. In the room, say both halves out loud: "worst case stays exponential, but the remaining-target prune cuts the real tree dramatically." That sentence is pure scorecard.

6Boards and strings are choice lists too — Word Search & partitioning

Once you see "choices at a node", backtracking escapes the arrays-of-numbers ghetto:

  • Word Search Medium — find a word by walking adjacent cells. The choice list is the four neighbors (a grid is a graph — Chapter 15's mantra); choose marks the cell visited, un-choose unmarks it. That unmark is the interesting delta from Chapter 15's islands: flood fill marks cells permanently because it asks "reachable, ever?" — Word Search must unmark, because a cell that failed for "ABCB" down one path may be exactly right for another. Permanent marks answer reachability; undoable marks answer paths. (Word Search II bolts Chapter 13's trie on top, pruning by "no word starts with this prefix" — pruning shears made of letters.)
  • Palindrome Partitioning Medium — split a string so every piece is a palindrome. The choice list at position i is "every prefix s[i..j] that is a palindrome"; choose appends the piece, explore continues from j + 1, un-choose pops it. The palindrome test is the prune: non-palindromic prefixes never enter the tree.
🎯
The tell: "does this word/path exist in a board" → grid DFS with un-marking. "Partition the string into all…" / "all ways to split…" → backtracking where a choice is a prefix, not an element. Both keep the small-n whisper: boards are ~6×6, strings ≤ 16 chars.

7Queens, constraints, and dignified retreat — prune-heavy search

N-Queens Hard is the pattern's graduation exam: place n queens on an n×n board so none attack. The framing that makes it easy is choosing the tree shape well — one queen per row, rows top to bottom. Then a node at depth r means "rows 0..r−1 are safely placed", the choice list is "columns in row r", and the prune is constraint checking: a column, a ↘ diagonal (r − c constant), or a ↗ diagonal (r + c constant) already claimed → refuse. Three hash sets, O(1) per test.

The drama is the retreat: sometimes every column in row r is attacked. That's not failure, that's information — the recursion returns, the parent un-chooses its queen and tries her one column to the right. Watch the auto-solver below do exactly this; the backtrack counter is the number of times a placed queen had to be taken back. Sudoku Solver Hard is the same machine with 9 choices per empty cell and row/column/box sets as the prune — bigger board, identical liturgy.

You can also play interviewer: click squares to place queens by hand and watch the attack rays find your conflicts.

Interactive · the queens board Click squares to place by hand, or auto-solve with visible backtracks
hand mode — click squares to place queens
6 × 6
queens on board
0
conflicts
0
placements tried
0
backtracks
0

8Know your budget — when backtracking is legal, and when DP swallows it

Backtracking's honest price list: O(n · 2ⁿ) for subset-shaped trees, O(n · n!) for permutation-shaped ones — "every candidate, copied once". Chapter 3's constraint sniffing turns that into a go/no-go call in seconds: 2ⁿ is fine to about n = 20; n! dies around n = 10. If the constraints say n ≤ 15, the interviewer has pre-approved your exponential — say so, out loud, and it counts as analysis.

The boundary to respect is with Chapter 20. If the problem wants the objects themselves — the actual subsets, the actual boards — the output is exponential and backtracking is the only game. But if it wants only a count or a best value ("how many ways", "minimum cost"), and the subproblems repeat, then enumerating every way is a war crime against the CPU: DP computes the count without touching the ways. Backtracking + caching on repeated states is memoization — which is to say, Chapter 20 is what happens when this chapter's tree starts rhyming with itself.

🎯
The tell, refined: "return all…" + n ≤ 20 → backtracking, no hesitation. "Count the ways" or "find the best" + large n → the tree has repeated subtrees; let Chapter 20's DP swallow it. The word all versus the word count is doing more routing than the rest of the sentence.

The pattern, as a whiteboard skeleton:

  1. 1Sort the input if duplicates exist or a prune needs order — "skip equal neighbors" and "break, not continue" are only legal on sorted choices.
  2. 2Set up shared state: res = [], one mutable path = [], plus any marks (used[], visited, attacked-sets).
  3. 3Write backtrack(state): first check "is this a complete candidate?" — if so, collect a copy and (usually) return.
  4. 4Enumerate the legal choices at this node — elements from start, unused values, neighbors, columns. The only part that changes between problems.
  5. 5Prune before recursing: refuse any choice you can prove is doomed (candidate > remaining, attacked square, dead prefix).
  6. 6Choose → explore → un-choose, and make the undo the exact mirror of the choose — every mutation gets its inverse.
  7. 7Trace a tiny input and count leaves — 2ⁿ subsets, n! permutations. A wrong count catches a missing pop faster than any debugger.

9Subsets — the skeleton that never changes

Subsets Medium is the flagship because it's the skeleton with nothing else attached — no size check, no dedup, no prune. Read the three commented lines in the loop; they are the entire pattern. Then read the permutations function directly below it and notice what changed: the choice list (start index became a used array) and the collect condition. Nothing else. That delta is the whole of Section 4, executable.

The Scala pane adds a treat: the pure-FP fold that builds all subsets in three lines. Admire it, then put it away — the interview wants the skeleton, because the skeleton is the one that generalizes to Combination Sum, Word Search, and N-Queens by swapping two lines.

⚠️
Say the copy out loud. When you write res.append(path[:]), narrate it: "copying, because path is shared and about to be mutated." Interviewers plant this bug in their heads before you arrive; defusing it unprompted is one of the cheapest verification points in the whole interview.