Backtracking
9 questions, and they are all the same three lines. What changes is the choices, the base case, and the pruning.
The skeleton
void backtrack(State path, Choices remaining) {
if (isComplete(path)) { record(path); return; }
for (Choice c : remaining) {
path.add(c); // CHOOSE
backtrack(path, next(c)); // EXPLORE
path.removeLast(); // UN-CHOOSE
}
}The un-choose is what makes it backtracking. path is shared mutable state across the whole search; without the removal, a sibling branch inherits the previous branch's choices. Verified: the no-restore variant of Word Search disagreed with the correct answer on 13 of 400 random boards — it fails sometimes, which is why it survives casual testing.
Three things vary, and only three
| What changes | Examples | |
|---|---|---|
| Choices | which candidates the loop iterates | array elements (Q1–Q5), grid neighbours (Q6), cut points (Q7), keypad letters (Q8), columns (Q9) |
| Base case | where you record | every node (Q1), at full depth (Q3), when a sum hits zero (Q2), when the string is consumed (Q7) |
| Pruning | when to abandon a branch | none (Q8), sum overflow (Q2), duplicate sibling (Q4, Q5), invalid piece (Q7), attacked cell (Q9) |
The start index decides three separate things
| Recursive call | Meaning | Used by |
|---|---|---|
backtrack(i, …) | the element may be reused | Combination Sum |
backtrack(i + 1, …) | each element used at most once | Subsets, Subsets II, Combination Sum II |
no start index, used[] instead | order matters — every element is a candidate every time | Permutations |
And in all three cases, starting the loop at the index forces non-decreasing order — which is what makes combinations unique without a deduplication pass. A start index exists to destroy ordering information; permutations need it kept, which is precisely why they don't have one.
Deduplication: three different rules
Getting these confused is the most common source of silently-wrong answers.
| Problem | Rule | Why |
|---|---|---|
| Subsets II / Comb Sum II | i > start && nums[i] == nums[i-1] | skip a repeat that is a sibling |
| Permutations II | i > 0 && nums[i] == nums[i-1] && !used[i-1] | no start index, so "sibling" means the twin is unplaced |
i > start, not i > 0. Verified on [1,2,2]: the correct guard yields 6 subsets, i > 0 yields 4, silently losing [2,2] and [1,2,2].
For Permutations II, both !used[i-1] and used[i-1] produce the correct set — the difference is pruning, measured at 92× on all-identical input.
continue vs break
break abandons the whole level, so it is only valid when failure is monotonic in the loop variable.
| Problem | Correct | Why |
|---|---|---|
| Combination Sum | break | sorted + positive, so a too-large candidate means all later ones are too |
| Palindrome Partitioning | continue | "ab" fails but "aba" succeeds — failure is not monotonic |
Verified: using break in Palindrome Partitioning on "aba" returns [["a","b","a"]] and loses [["aba"]].
N-Queens: make the check O(1)
Cells on a ↗ diagonal share r + c; cells on a ↘ diagonal share r − c. So three sets replace an O(n) scan — and since the validity check runs at every node, that removes a factor of n from the whole search.
The row constraint is handled better still: placing one queen per row designs it away, so rows never need checking at all.
The traps
| Trap | Symptom |
|---|---|
| Forgetting the un-choose | Sibling branches inherit stale state — fails intermittently |
Storing path instead of a copy | Every result points at the same eventually-empty list |
start + 1 instead of i + 1 (Q1) | Same subset generated twice |
i > 0 instead of i > start (Q4, Q5) | Valid subsets silently dropped |
break where continue is needed (Q7) | Valid partitions dropped |
| Comparing only at full length (Q6) | Dead branches extended to depth L instead of dying at depth 1 |
| Forgetting the board restore (Q6) | Cells stay blocked; false negatives |
| Removing from only some sets (Q9) | Over-constrained; too few solutions |
| No empty-input guard (Q8) | Returns [""] instead of [] |
Verification
Every snippet compiled and cross-checked against an independent implementation — 2,800+ randomized cases:
- Q1 across backtracking, bitmask and iterative doubling, with the count asserted as exactly
2^n - Q3 across
used[]and swap-based versions, count asserted asn! - Q4 and Q5 against generate-and-deduplicate references, on heavily duplicated inputs
- Q6 against a
boolean[][]reference, plus the caller's board verified untouched afterwards - Q7 against a generate-all-cuts-and-filter reference
- Q9 counts matched the known sequence 1, 0, 0, 2, 10, 4, 40, 92, 352 for
n = 1..9, and every returned board was independently re-validated for row, column and diagonal conflicts
Five deliberately-broken variants confirm the traps are real:
i > 0in Subsets II returns 4 subsets instead of 6 on[1,2,2]breakin Palindrome Partitioning loses[["aba"]]- no board restore in Word Search disagreed on 13 of 400 boards
- no empty guard in Letter Combinations returns a one-element list instead of an empty one
- the
O(n)-scan N-Queens agrees on counts, confirming the encodings prune identically