LC Patterns — Backtracking

Previous: binary-search, two-pointers, sliding-window, 1d-dp, arrays-strings, trees-bsts · Code: C++
Per-pattern template: What it is → Intuition → Approach → Challenges & pitfalls → LC problems (max 3) → Worked examples.

The Master Insight (read before the patterns)

Backtracking is DFS over a decision tree of partial solutions: at each node you choose an option, explore the consequences recursively, then unchoose to put the state back for the next option. That choose–explore–unchoose symmetry is the entire mechanic — every bug in this topic is a broken symmetry.

When backtracking is the right tool: the problem asks you to enumerate all solutions (subsets, permutations, boards), or to find any/best solution in a space with no overlapping subproblems to exploit. If subproblems do overlap and you only need a count or an optimum, that's DP — the boundary question to ask out loud: "am I outputting the solutions themselves (can't beat exponential — the output is exponential), or just counting them (DP may collapse it)?"

The two structural questions that decide your skeleton:

  1. Does order matter? No (subsets/combinations) → iterate with a start index and never look backward. Yes (permutations) → track used elements.
  2. Are there duplicates in the input? Sort, then skip equal values at the same tree depth: if (i > start && a[i] == a[i-1]) continue; — the one-line dedup that removes duplicate branches while still letting duplicate elements be used within one path.

Since the space is exponential, the craft is pruning: check constraints at the earliest moment (before recursing, not at the leaf), sort to enable early break, and carry incremental state (a running sum, column flags) instead of recomputing.

§0The Universal Template & Pitfalls

void bt(State& path, Choices at this level, Results& out) {
    if (isComplete(path)) { out.push_back(path); return; }   // COPY the path into results
    for (choice : validChoices()) {                          // prune here, not at the leaf
        apply(choice, path);          // choose
        bt(path, next, out);          // explore
        undo(choice, path);           // unchoose — mirror image of apply
    }
}
  1. Broken undo symmetry: every apply needs exactly one undo on every exit path — an early continue/return between them silently corrupts all later branches. Structure it so choose/undo hug the recursive call with nothing between.
  2. Aliasing on record: out.push_back(path) copies (good — C++ copies by value here); storing references or moving the live path records garbage. Conversely, passing path by value into recursion "works" but costs O(n) per node — pass by reference + undo; say the trade-off.
  3. Same-level dedup vs same-path reuse: i > start && a[i] == a[i-1] skips duplicates across siblings (after sorting) while still allowing [1,1,2]-style multi-use within one path. Writing i > 0 instead kills legitimate paths — the classic subtle wrong answer (90/40/47 all test it).
  4. start-index vs used[] confusion: a start index ⇒ combinations (each element considered once, order canonical); used[] ⇒ permutations (all orderings). Mixing them either duplicates output or misses it.
  5. Complexity statements: subsets O(2ⁿ·n), permutations O(n!·n), combinations O(C(n,k)·k) — the ·n is the copy cost per solution. Say these unprompted; "exponential, and that's optimal because the output is exponential" is the senior framing.
  6. Recursion depth & global state: depth ≤ n (fine); shared mutable state (board, column flags) must be undone, not copied — that's the whole point of backtracking over brute-force regeneration.

1Subsets (the power set)

What it is: Enumerate every subset of a set: at each element either include it or not (a binary decision tree), or equivalently — at each level, pick which next element to add from start onward and record every node, not just the leaves.

Intuition: Two mental models give identical output: include/exclude (each element = one binary choice → 2ⁿ leaves) and start-index growth (every node of the tree is a subset; the loop for i in [start, n) extends the current subset in canonical order — no duplicates because you never look backward). Learn both; the start-index one generalizes to every pattern below. With duplicate inputs (90): sort + same-level skip (§0.3).

Approach:

void bt(vector<int>& a, int start, vector<int>& path, vector<vector<int>>& out) {
    out.push_back(path);                          // record EVERY node — subsets, not leaves
    for (int i = start; i < (int)a.size(); ++i) {
        if (i > start && a[i] == a[i-1]) continue; // dedup (needs sorted input; for 90)
        path.push_back(a[i]);
        bt(a, i + 1, path, out);
        path.pop_back();
    }
}

Challenges & pitfalls

recording at every node vs only at leaves (subsets record always; combinations record at size k — know which); the i + 1 (not start + 1 — the off-by-one that turns subsets into garbage); bitmask enumeration (for mask in 0..2ⁿ) as the iterative alternative — worth naming for n ≤ 20.

LC 78 SubsetsLC 90 Subsets IILC 784 Letter Case Permutation
Example 1: Subsets (LC 78)nums = [1, 2, 3].
  • Record the current path at every node, then extend by each element from start onward:
    • [] (recorded) → add 1 → [1] → add 2 → [1, 2] → add 3 → [1, 2, 3]. Back up.
    • from [1], skip 2, add 3 → [1, 3]. Back up to [].
    • add 2 → [2] → add 3 → [2, 3]. add 3 → [3].
  • All subsets: [], [1], [1,2], [1,2,3], [1,3], [2], [2,3], [3] — 2³ = 8 of them. ✓ Using i + 1 (never looking back) is what keeps {1,2} and {2,1} from both appearing.

2Combinations (choose k, order irrelevant)

What it is: All ways to choose k items from n — the subsets tree cut at exactly size k. The start-index discipline keeps each combination in canonical order, so {1,2} and {2,1} are one branch, not two.

Intuition: Combinations = subsets + a size target, which unlocks the signature prune: if the remaining pool can't fill the quota, stop — if (n - i + 1 < k - path.size()) break;. Because the loop goes ascending, break (not continue) kills all further siblings at once. This "count the remaining budget" prune generalizes: sum-based problems prune on the remaining target (Pattern 3), boards prune on constraint flags (Pattern 7).

Approach:

void bt(int n, int k, int start, vector<int>& path, vector<vector<int>>& out) {
    if ((int)path.size() == k) { out.push_back(path); return; }
    for (int i = start; i <= n - (k - (int)path.size()) + 1; ++i) {  // feasibility prune in the bound
        path.push_back(i);
        bt(n, k, i + 1, path, out);
        path.pop_back();
    }
}

Challenges & pitfalls

the prune bound's off-by-one (derive it — "need k - size more, so the last viable start is n - needed + 1" — don't pattern-match it); measuring the prune's worth (without it 77 still passes; in interviews say it exists and why it matters at scale); recording requires the size check first — recording at every node here is Pattern 1's behavior, a different problem.

LC 77 CombinationsLC 216 Combination Sum IIILC 1286 Iterator for Combination
Example 1: Combinations (LC 77)n = 4, k = 2.
  • Grow the path with ascending starts; record when it reaches size 2:
    • start 1 → [1,2], [1,3], [1,4].
    • start 2 → [2,3], [2,4].
    • start 3 → [3,4].
    • start 4 → can't reach size 2 (only one element left) → pruned.
  • Answer [[1,2], [1,3], [1,4], [2,3], [2,4], [3,4]]. ✓ The ascending start index means each pair appears exactly once, in sorted order.

3Combination Sum Family (target-driven, reuse rules)

What it is: Find combinations summing to a target, under a reuse rule: elements reusable unlimited times (39 — recurse with i, not i+1), or each element used once with duplicate values in the input (40 — recurse i+1 + same-level dedup). The recursion carries remaining = target − chosen so far.

Intuition: The single index passed downward encodes the reuse policy: i = "may pick me again", i+1 = "move on" — one character switches the problem. Sorting buys the strong prune: once a[i] > remaining, every later candidate is too big → break. This family is the cleanest place to see the DP boundary: counting these combinations is coin-change; listing them is irreducibly exponential — same tree, different question.

Approach:

void bt(vector<int>& a, int start, int rem, vector<int>& path, vector<vector<int>>& out) {
    if (rem == 0) { out.push_back(path); return; }
    for (int i = start; i < (int)a.size() && a[i] <= rem; ++i) {   // sorted ⇒ break via loop cond
        if (i > start && a[i] == a[i-1]) continue;                 // for 40 (dups, single use)
        path.push_back(a[i]);
        bt(a, /*39:*/ i /*40: i+1*/, rem - a[i], path, out);
        path.pop_back();
    }
}

Challenges & pitfalls

i vs i+1 (the whole reuse semantics — state your choice and why); applying the dedup line to 39 (wrong — 39 has distinct input; the dedup is for 40's duplicated values); negative/zero elements break the a[i] <= rem prune (constraints keep inputs positive — notice aloud); unbounded reuse depth is safe only because elements are ≥ 1.

LC 39 Combination SumLC 40 Combination Sum IILC 216 Combination Sum III
Example 1: Combination Sum (LC 39)candidates = [2, 3, 6, 7], target = 7, elements reusable.
  • Carry remaining, and recurse with i (not i+1) so an element can be reused:
    • pick 2 (rem 5) → pick 2 again (rem 3) → pick 3 (rem 0) → record [2, 2, 3].
    • back up: from rem 5, pick 3... doesn't reach 0 cleanly here; explore others.
    • pick 7 (rem 0) → record [7].
  • Answer [[2, 2, 3], [7]]. ✓ Recursing with i is what lets 2 repeat; sorting lets the loop break as soon as a candidate exceeds remaining.

4Permutations (order matters)

What it is: All orderings of the elements: at each level choose any unused element. Two implementations: a used[] boolean array (clear, general), or in-place swapping (swap(a[level], a[i]), recurse, swap back — O(1) extra space, subtler).

Intuition: Order mattering kills the start-index trick — every level must consider all elements, filtered by "already on the path." The tree has n! leaves and only leaves are solutions. Duplicate inputs (47) need the elegant rule: sort, then if (used[i-1] == false && a[i] == a[i-1]) continue — "among equal values, use them left-to-right": if the earlier twin isn't currently used, picking the later one now would recreate a branch the earlier one already generated.

Approach:

void bt(vector<int>& a, vector<bool>& used, vector<int>& path, vector<vector<int>>& out) {
    if (path.size() == a.size()) { out.push_back(path); return; }
    for (int i = 0; i < (int)a.size(); ++i) {
        if (used[i]) continue;
        if (i > 0 && a[i] == a[i-1] && !used[i-1]) continue;   // 47: equal values left-to-right
        used[i] = true;  path.push_back(a[i]);
        bt(a, used, path, out);
        used[i] = false; path.pop_back();
    }
}

Challenges & pitfalls

the !used[i-1] dedup direction (both !used[i-1] and used[i-1] "work" but prune different branches — be able to explain yours); the swap-based version breaks ordering guarantees and fights with dedup (prefer used[] when duplicates exist); constraint-heavy variants (526) live or die on pruning position — check the constraint before recursing, not at the leaf.

LC 46 PermutationsLC 47 Permutations IILC 526 Beautiful Arrangement
Example 1: Permutations (LC 46)nums = [1, 2, 3].
  • At each level pick any unused element:
    • start 1 → 2 → 3 gives [1,2,3]; back up, 1 → 3 → 2 gives [1,3,2].
    • start 2 → [2,1,3], [2,3,1]. start 3 → [3,1,2], [3,2,1].
  • All 3! = 6 orderings. ✓ The used[] array (not a start index) is what lets a level revisit earlier elements, which is exactly what "order matters" requires.

5String Segmentation (choose the next cut)

What it is: Partition a string by choosing where each segment ends: palindrome partitioning (every segment a palindrome), IP restoration (exactly 4 segments, each 0–255), word-break-II (every segment in a dictionary). The choice at each level = "how long is the next piece?"

Intuition: The decision tree's branching factor is the segment length, and the state is just the current position — which is why these compose beautifully with precomputation and memoization: 131 precomputes a palindrome table (O(1) validity checks); in 140 the positions repeat across branches → memoize position → sentence-list, the textbook backtracking→DP hybrid. This is the same "partition by the next piece" move as the DP file's segmentation pattern — DP counts feasibility, backtracking materializes the pieces.

Approach:

void bt(string& s, int pos, vector<string>& path, vector<vector<string>>& out) {
    if (pos == (int)s.size()) { out.push_back(path); return; }
    for (int len = 1; pos + len <= (int)s.size(); ++len) {
        if (!valid(s, pos, len)) continue;        // palindrome / 0-255 / dictionary check
        path.push_back(s.substr(pos, len));
        bt(s, pos + len, path, out);
        path.pop_back();
    }
}

Challenges & pitfalls

IP-specific rules concentrate the difficulty (no leading zeros — "0" ok, "01" not; ≤ 3 digits; exactly 4 segments — prune when segments-left can't consume the remaining length); substr copies — for hot paths pass indices and materialize only on record; knowing when to add the memo (140: yes, positions repeat; 131: palindrome table yes, memoizing partitions no — output dominated).

LC 131 Palindrome PartitioningLC 93 Restore IP AddressesLC 140 Word Break II
Example 1: Palindrome Partitioning (LC 131)s = "aab", every piece must be a palindrome.
  • At each position, choose how long the next piece is; only cut if that piece is a palindrome:
    • piece "a" (pos 0–0) → piece "a" (pos 1–1) → piece "b" (pos 2–2) → whole string consumed → record ["a", "a", "b"].
    • back up: piece "aa" (pos 0–1, a palindrome) → piece "b" → record ["aa", "b"].
    • piece "aab" isn't a palindrome → not cut.
  • Answer [["a","a","b"], ["aa","b"]]. ✓ The choice at each level is just the next segment's length, filtered by the palindrome check.

6Grid Path Search (word search / mark-and-restore)

What it is: DFS from a cell through neighbors to match a target (a word, a path, max gold collected), marking cells visited in place and restoring them on the way out — backtracking where the "path state" is written into the board itself.

Intuition: The board is the used[] array: overwrite the cell with a sentinel ('#') on entry, restore the letter on exit — O(1) space, and the choose/undo symmetry is literal. Word Search II (212) is the marquee composition: searching many words cell-by-cell repeats prefix work → put the dictionary in a trie and walk board and trie together, pruning the instant the current path isn't a prefix of anything.

Approach:

bool dfs(vector<vector<char>>& b, int i, int j, string& w, int k) {
    if (k == (int)w.size()) return true;
    if (i < 0 || i >= (int)b.size() || j < 0 || j >= (int)b[0].size() || b[i][j] != w[k])
        return false;                              // bounds + match check = the prune
    char c = b[i][j]; b[i][j] = '#';               // choose: mark visited
    bool found = dfs(b,i+1,j,w,k+1) || dfs(b,i-1,j,w,k+1)
              || dfs(b,i,j+1,w,k+1) || dfs(b,i,j-1,w,k+1);
    b[i][j] = c;                                   // unchoose: restore
    return found;
}

Challenges & pitfalls

restoring on every exit (the || short-circuit conveniently exits after restore here — but restructure carefully if you add logic); bounds-check ordering (check before reading b[i][j]); 212 without the trie is the naive trap (per-word DFS = TLE; the trie-walk is the point); early-exit extras for 79 (letter-frequency pre-check, searching from the rarer end of the word) are name-drop-worthy prunes.

LC 79 Word SearchLC 212 Word Search IILC 1219 Path with Maximum Gold
Example 1: Word Search (LC 79) — board
A B C E
S F C S
A D E E

and word "ABCCED".

  • Start at the A at (0,0) — it matches w[0]. Mark it #, recurse to neighbors for B:
    • B at (0,1) ✓ → C at (0,2) ✓ → next C: down to (1,2) ✓ → E at (2,2) ✓ → D at (2,1) ✓ → word fully matched → true.
  • Each cell was marked # on entry (so the path can't reuse it) and restored on the way back out. ✓ The board itself is the visited-set.

7Constraint-Satisfaction Boards (N-Queens, Sudoku)

What it is: Place items on a board subject to global constraints: N-Queens (no shared row/column/diagonal), Sudoku (row/column/box uniqueness). Recurse position by position; at each, try every value/placement that passes the constraints; a dead end → backtrack.

Intuition: The efficiency insight is O(1) constraint checking via incremental bookkeeping: don't re-scan the board — keep sets/flags updated as you place and remove. N-Queens' diagonals collapse to two indices: r + c (anti-diagonal) and r − c + n (main diagonal); a placement flips three booleans, a conflict check reads three. Placing row-by-row bakes the row constraint into the recursion structure itself — structure eliminates constraints before code checks them.

Approach (N-Queens core):

void place(int r, int n, vector<int>& colOf,
           vector<bool>& col, vector<bool>& d1, vector<bool>& d2, int& count) {
    if (r == n) { ++count; return; }               // or materialize the board here
    for (int c = 0; c < n; ++c) {
        if (col[c] || d1[r + c] || d2[r - c + n]) continue;
        col[c] = d1[r + c] = d2[r - c + n] = true;  colOf[r] = c;
        place(r + 1, n, colOf, col, d1, d2, count);
        col[c] = d1[r + c] = d2[r - c + n] = false;
    }
}

Challenges & pitfalls

the diagonal index formulas (derive them live: "cells on an anti-diagonal share r+c" — memorized-only versions crumble under "why?"); Sudoku's cell ordering (row-major works; the most-constrained-cell-first heuristic is the optimization to name); undoing all three flags (miss one and the board poisons later branches — §0.1); count-only (52) vs materialize (51): same recursion, different record.

LC 51 N-QueensLC 52 N-Queens IILC 37 Sudoku Solver
Example 1: N-Queens (LC 51)n = 4.
  • Place one queen per row, skipping columns/diagonals already taken:
    • Row 0: try column 0 → mark col 0, diagonals. Row 1: columns 0/1 conflict → try column 2 or 3; follow column 2 → row 2: all columns conflict → dead end → back up.
    • The search settles on two valid boards, e.g. queens at columns [1, 3, 0, 2] and its mirror [2, 0, 3, 1]: . Q . . . . Q . . . . Q Q . . . Q . . . . . . Q . . Q . . Q . .
  • Two solutions. ✓ Placing row by row makes the row constraint free; the r+c / r−c flags make each diagonal check O(1).

8Constrained Sequence Generation (parentheses, phone letters)

What it is: Build strings character by character where a running invariant limits each next character: valid parentheses (may add ( while any remain; may add ) only if open > close so far), phone-number letters (a pure cartesian product — each digit contributes its letter set).

Intuition: Generate-then-filter would enumerate 2²ⁿ strings; constrain-during-generation walks only the valid ones — the invariant (close ≤ open ≤ n) prunes at the decision point, so every leaf reached is a valid answer, zero waste. This "invariant carried in the parameters" is the trees file's top-down state, driving generation instead of evaluation. Output size for 22 is the Catalan number Cₙ — name it when asked for complexity.

Approach:

void gen(int open, int close, int n, string& cur, vector<string>& out) {
    if ((int)cur.size() == 2 * n) { out.push_back(cur); return; }
    if (open < n)     { cur.push_back('('); gen(open + 1, close, n, cur, out); cur.pop_back(); }
    if (close < open) { cur.push_back(')'); gen(open, close + 1, n, cur, out); cur.pop_back(); }
}

Challenges & pitfalls

the two guard conditions are the solution (get close < open wrong — e.g. close < n — and you generate garbage; derive it from "the prefix must never have more ) than ("); 17's edge case (empty digits → empty list, not a list holding ""); complexity of output-sized problems ("optimal because we do O(1) amortized work per emitted character" — §0.5 framing).

LC 22 Generate ParenthesesLC 17 Letter Combinations of a Phone Number
Example 1: Generate Parentheses (LC 22)n = 3.
  • Track open and close counts. Add ( while open < 3; add ) only while close < open:
    • Always-open first: ((( then forced closes )))"((()))".
    • Backing up and interleaving gives "(()())", "(())()", "()(())", "()()()".
  • Five results (the 3rd Catalan number). ✓ Because close < open gates every ), every generated string is valid — nothing to filter afterward.

9Bucket Partitioning (k equal-sum subsets)

What it is: Split items into k groups meeting a per-group condition (equal sums, matchstick square sides, fair cookie loads): for each item choose which bucket it joins — or, the dual view, fill buckets one at a time to the target. The hardest mainstream backtracking family (NP-hard in general — say so).

Intuition: The pruning arsenal decides feasibility in practice, and reciting it is the interview: (1) sort descending — big items fail fast, shrinking the tree near the root; (2) skip equal buckets — placing item x into empty bucket 3 mirrors empty bucket 2: if (bucket[j] == bucket[j-1]) continue; (symmetry breaking); (3) fail-fast arithmetic — total % k ≠ 0 → false, any item > target → false; (4) if an item fails in an empty bucket, no later arrangement saves it → abort the branch. The bucket-by-bucket dual + memo on item-bitmask is the DP escalation for tight cases — name it.

Approach:

bool bt(vector<int>& a, int idx, vector<int>& bucket, int target) {
    if (idx == (int)a.size()) return true;         // all placed (sums pre-validated)
    for (int j = 0; j < (int)bucket.size(); ++j) {
        if (j > 0 && bucket[j] == bucket[j-1]) continue;   // symmetric buckets: try once
        if (bucket[j] + a[idx] > target) continue;
        bucket[j] += a[idx];
        if (bt(a, idx + 1, bucket, target)) return true;
        bucket[j] -= a[idx];
        if (bucket[j] == 0) break;                 // failed in an empty bucket → hopeless
    }
    return false;
}

Challenges & pitfalls

each prune needs its argument (symmetric-bucket and empty-bucket-break are the two interviewers challenge — rehearse both); sorting descending is not optional (ascending versions TLE); distinguishing from 416 (k = 2 → subset-sum DP — the k = 2 special case escapes to polynomial; general k doesn't); early total/max validation before any recursion.

LC 698 Partition to K Equal Sum SubsetsLC 473 Matchsticks to SquareLC 2305 Fair Distribution of Cookies
Example 1: Partition to K Equal Sum Subsets (LC 698)nums = [4, 3, 2, 3, 5, 2, 1], k = 4.
  • Total = 20, so each of the 4 buckets must reach 20 / 4 = 5 (fail fast: if the total didn't divide evenly, return false immediately).
  • Sort descending [5, 4, 3, 3, 2, 2, 1] and place each item into a bucket that still has room:
    • 5 → bucket A (5, full). 4 → bucket B (needs 1 more). 3 → bucket C. 3 → bucket D. 2 → into C (5, full). 2 → into D (5, full). 1 → into B (5, full).
    • Buckets: [5], [4,1], [3,2], [3,2] — all sum to 5 → true.
  • Sorting descending makes the big items commit early, and the symmetric-bucket / empty-bucket prunes cut the search dramatically. ✓

Summary Table — Recognition Cheat Sheet

#PatternOrder matters?Key mechanicTrigger phrase
1SubsetsNostart index; record every node"all subsets", "power set"
2CombinationsNostart index + size target; budget prune"choose k of n"
3Combination sumNoi (reuse) vs i+1 (once); sort + break"combinations summing to target"
4PermutationsYesused[]; !used[i-1] dedup"all orderings/arrangements"
5String segmentationpositionalcut positions; memo when positions repeat"partition string", "restore IP"
6Grid path searchpositionalmark-in-place + restore; trie for multi-word"word exists in board"
7Constraint boardspositionalincremental O(1) checks; r+c / r−c diagonals"N-Queens", "Sudoku"
8Constrained generationYesinvariant guards at decision point"generate all valid ⟨X⟩"
9Bucket partitioningNo (destinations)sort desc; symmetric-bucket & empty-bucket prunes"k equal groups"
Study order: 1 → 2 → 3 (the start-index family — one skeleton, three twists) → 4 (used[] + the dedup rule) → 8 (invariant-driven generation) → 5 → 6 (board-as-state) → 7 → 9 (the pruning capstone).
The interview sentence (per your communication playbook §1.3): "This asks for all solutions, so it's backtracking — the output itself is exponential, and that's the lower bound. My decision at each level is ⟨X⟩, order ⟨does/doesn't⟩ matter so I'll use ⟨used[] / a start index⟩, duplicates are handled by sort + same-level skip, and I'll prune on ⟨constraint⟩ before recursing."