latency.lab
Algorithms · Topic 10

Backtracking

Level: foundationTool: choose / explore / un-choose, with early pruningTime: ~70 min

By the end you can

Backtracking is systematic brute force: explore every choice, but the instant a partial choice can't possibly lead anywhere valid, abandon it and try the next one instead of continuing to build on top of it. Every problem in this topic is the same three-step template wearing different clothes — the differences are all in what "choice" means and what "can't possibly work" means.

The template: choose, explore, un-choose

Almost every function in this topic has the same shape: mutate some shared state to represent "I've made this choice," recurse to explore everything that follows from it, then undo that exact mutation before trying the next choice. That undo step is what makes backtracking different from plain recursion — you're reusing one mutable structure (a path vector, a "used" array, the grid itself) across the entire search tree instead of copying it at every level, and the undo is what keeps that safe.

the-template.cpp
void backtrack(/* position in the search */) {
    if (/* complete solution */) { record it; return; }
    for (each candidate choice at this point) {
        if (choice is obviously invalid) continue; // prune
        make the choice   // mutate shared state
        backtrack(next position)
        undo the choice    // <-- the line that's easy to forget
    }
}
The habit to build

Whenever you write a line that mutates shared state before a recursive call, immediately write its undo right after the call returns, before you fill in anything else. Forgetting the undo is the single most common bug in this entire topic — Permutations' used[i] = false, Word Search's board-cell restore, and N-Queens' clearing three tracking arrays are all the exact same discipline.

Pruning is what separates backtracking from brute force

Generating every candidate and checking validity only at the end is still technically correct — it's just wasteful. The real skill in this topic is pushing the validity check as early as possible, so an entire doomed subtree gets skipped instead of explored. Combinations prunes when too few elements remain to ever reach length k. Combination Sum prunes (using sorted order) the instant a candidate would overshoot the target. Palindrome Partitioning prunes the instant a candidate piece isn't a palindrome, rather than building a complete partition and checking every piece afterward — for a string with no repeated characters, that difference is the gap between exploring one path and exploring billions.

[] [2] [3] [2,2] ✕
Combination Sum on candidates [2, 3], target 4. Choosing 3 then 3 again would total 6 — overshooting the target — so that branch (dashed, red) is pruned the instant the sum exceeds 4, without ever exploring what comes after it. The 2 → 2 branch (teal) survives to a valid leaf.

The duplicate-handling pattern

Subsets II and Permutations II both solve the same meta-problem: the input has duplicate values, and naively running Subsets or Permutations unmodified produces duplicate outputs. The fix in both cases is sort first, then skip a duplicate candidate at the same decision point — but "same decision point" means something subtly different in each: for subsets, it's "the same recursion level" (i > start); for permutations, it's "this identical value's earlier copy is still unused" (!used[i-1]). Getting the exact skip condition right — not just "skip if it equals the previous element," full stop — is what actually distinguishes these from their non-duplicate-safe cousins.

Constraint tracking instead of full re-validation

N-Queens II could check every new queen against every previously placed queen on each attempt — that works, but rescans state that a placement earlier already resolved. Tracking three boolean arrays (occupied columns, and both diagonal directions) turns "is this placement legal" from a scan into a lookup — the same shift from "recompute" to "maintain incrementally" that shows up constantly once you're looking for it.

Common mistakes

  • Forgetting the undo step — mutating a path, a used-array, or a grid cell without restoring it before the next iteration corrupts every sibling branch that runs afterward.
  • Checking validity only at the leaf instead of pruning mid-construction — technically correct, but throws away most of the benefit backtracking has over pure brute force.
  • Deduplicating wrong — skipping "any index with a value seen before" (globally) instead of "the same value at this specific decision point" silently drops valid results, not just duplicate ones.
  • Allowing reuse when the problem forbids it (or vice versa) — the only difference between Combination Sum (reuse allowed) and Combination Sum II (reuse forbidden) is recursing to the same index versus the next one; mixing these up is an easy, quiet mistake.

Takeaways

  • Backtracking is choose → explore → un-choose, reusing one mutable structure across the whole search instead of copying state at every level.
  • Pruning mid-construction (not just checking at the leaf) is what makes backtracking faster than plain brute force — the earlier a doomed branch is cut, the more work is saved.
  • Duplicate-handling means sorting first and skipping a repeated value at the *same decision point* — the exact definition of "same decision point" differs between subsets, combinations, and permutations.
  • Tracking constraints incrementally (column/diagonal sets, a running remaining-target) beats re-validating everything from scratch on every step.

Try it: the foundation template

Subsets is the cleanest version of choose/explore/un-choose — get this automatic here, since every other problem in this topic is a variation on it.

Live

Loading starter code…

Try it: pruning mid-construction

Combination Sum — the sorted-order prune here is what separates backtracking from "generate everything, filter at the end."

Live

Loading starter code…

Try it: the undo step on a grid

Word Search — the same choose/un-choose discipline as Permutations' used-array, just with the grid itself as the shared mutable state.

Live

Loading starter code…

Keep going

That's 3 of the 12 problems in this topic. Subsets II and Permutations II are worth doing next — both apply the same template to inputs with duplicates, which is where the exact skip condition really matters. See the full Backtracking set →

Checkpoint · Backtracking

5 questions · pass at 70%

Q01
Every backtracking function in this topic follows a choose / explore / un-choose template. Why is the "un-choose" (undo) step necessary at all, rather than just letting the recursive call return?
WhyBacktracking's efficiency comes specifically from NOT copying state at every recursive call -- one shared structure is mutated and restored throughout. Skipping the restore means every subsequent sibling branch operates on corrupted state instead of the clean state it should have started from.
Q02
Palindrome Partitioning prunes the instant a candidate piece fails the palindrome check, rather than generating a complete partition and checking all pieces at the end. Why does this matter so much for a string with no repeated characters?
WhyOnce a prefix piece fails to be a palindrome, EVERY partition that starts with that same doomed piece is also invalid -- pruning there eliminates that whole subtree in one step, instead of independently discovering the same failure at the end of billions of separate complete partitions.
Q03
Subsets II skips a duplicate candidate when "i > start && nums[i] == nums[i-1]" (same recursion level). Why is "same level", specifically, the right condition -- rather than just skipping any index whose value has appeared anywhere before?
WhyThe goal isn't "never reuse a value that's appeared before" -- it's "don't let two sibling branches at the same decision point produce the exact same subset." A global skip would incorrectly exclude valid deeper uses of a duplicate value; the same-level-only skip is what correctly allows {2,2} while still avoiding a duplicate {2} from two different starting positions.
Q04
Combination Sum recurses to the SAME index (not index + 1) after choosing a candidate, since a number may be reused. What would change if Combination Sum II's rule (recurse to index + 1) were used instead, on a problem where reuse is actually allowed?
WhyThe choice between recursing to the same index versus the next index IS the entire difference between "reuse allowed" and "reuse forbidden." Using the wrong one doesn't crash -- it just quietly produces an incomplete (or, in the other direction, duplicate-containing) result set.
Q05
N-Queens II tracks occupied columns and both diagonal directions in three boolean arrays, checked and updated on every placement. What's the actual benefit over just comparing each new queen against every previously placed queen directly?
WhyBoth approaches explore the identical search tree and find the identical solutions -- the arrays only change the COST of each conflict check, from scanning all previously placed queens down to three array lookups. It's a constant-factor improvement, not a different algorithm, but at a large enough search tree that constant factor is genuinely noticeable.

Finished Backtracking?

Pass the quiz to complete it automatically.