Learning/Backtracking

Backtracking

9 questions, and they are all the same three lines. What changes is the choices, the base case, and the pruning.

The skeleton

Choose, explore, un-choose
Choose, explore, un-choose

Java
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 changesExamples
Choiceswhich candidates the loop iteratesarray elements (Q1–Q5), grid neighbours (Q6), cut points (Q7), keypad letters (Q8), columns (Q9)
Base casewhere you recordevery node (Q1), at full depth (Q3), when a sum hits zero (Q2), when the string is consumed (Q7)
Pruningwhen to abandon a branchnone (Q8), sum overflow (Q2), duplicate sibling (Q4, Q5), invalid piece (Q7), attacked cell (Q9)

The start index decides three separate things

The start index controls reuse, order and duplicates
The start index controls reuse, order and duplicates

Recursive callMeaningUsed by
backtrack(i, …)the element may be reusedCombination Sum
backtrack(i + 1, …)each element used at most onceSubsets, Subsets II, Combination Sum II
no start index, used[] insteadorder matters — every element is a candidate every timePermutations

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.

ProblemRuleWhy
Subsets II / Comb Sum IIi > start && nums[i] == nums[i-1]skip a repeat that is a sibling
Permutations IIi > 0 && nums[i] == nums[i-1] && !used[i-1]no start index, so "sibling" means the twin is unplaced

Sort, then skip repeats at the same level
Sort, then skip repeats at the same level

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.

ProblemCorrectWhy
Combination Sumbreaksorted + positive, so a too-large candidate means all later ones are too
Palindrome Partitioningcontinue"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)

Two subtractions turn diagonals into O(1) lookups
Two subtractions turn diagonals into O(1) lookups

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

TrapSymptom
Forgetting the un-chooseSibling branches inherit stale state — fails intermittently
Storing path instead of a copyEvery 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:

Five deliberately-broken variants confirm the traps are real: