Backtracking Explore & Undo
Choose โ explore โ unchoose. Backtracking is DFS on the decision tree โ it systematically tries every valid option, undoes the choice, and tries the next. One template handles subsets, permutations, combinations, N-Queens, Sudoku, and every "generate all valid X" problem.
What is Backtracking?
Backtracking is a systematic way to explore all possible configurations by building candidates incrementally and abandoning ("pruning") a candidate as soon as it's clear it can't lead to a valid solution. It's DFS on a decision tree: at each node, you make a choice, recurse to explore the consequences, then undo the choice and try the next option. The three steps โ choose, explore, unchoose โ form a universal template that handles subsets (include or skip each element), permutations (place each unused element), combinations (pick k from n), and constraint satisfaction (N-Queens, Sudoku, word search). Backtracking is inherently exponential โ for n elements, subsets = O(2โฟ), permutations = O(n!), combinations = O(C(n,k)) โ but pruning invalid branches early makes it practical. In interviews, the template is always the same; the only thing that changes is the decision at each node and the pruning condition.
How It Thinks
start parameter controls which elements are availablestart = i + 1 โ never revisit earlier elements โ no duplicates. For permutations: start = 0 + used[] โ any unused element can be placed.if (i > start && nums[i] == nums[i-1]) continue; โ prevents choosing the same value at the same tree level. This works for subsets II, combinations II, permutations II.result.add(new ArrayList<>(path)) โ the path is mutable and will be modified by unchoose. Without copying, all entries in result point to the same (eventually empty) list.The Universal Backtracking Template
| Problem | isGoal() | nextStart | Prune |
|---|---|---|---|
| Subsets | Always (collect at every node) | i + 1 | โ |
| Combinations (k) | path.size() == k | i + 1 | remaining < k - path.size() |
| Permutations | path.size() == n | 0 + used[] | used[i] |
| Combination Sum | remaining == 0 | i (reuse) or i + 1 (no reuse) | remaining < 0 |
| N-Queens | row == n | next row, col 0..n-1 | column/diagonal attacked |
- The only things that change: (1) what you add to the path, (2) when the path is "complete," (3) what the next valid choices are, and (4) when to prune.
- Master the template once, and every problem is a fill-in-the-blanks exercise.
Subsets (Power Set)
Permutations
start = i + 1
Never revisit earlier elements. Order doesn't matter: 2 and 1 are the same subset. Loop from start forward.
start = 0 + used[]
Any unused element can be placed next. Order matters: [1,2] and [2,1] are different permutations. Loop from 0, skip used.
Combinations & Combination Sum
i (not i+1) for reusei), Combination Sum II does NOT (i+1) and needs dedup (if (i > start && cands[i] == cands[i-1]) continue). Mixing up i vs i+1 is the #1 backtracking bug.
N-Queens & Sudoku
Constraint-satisfaction problems use the same backtracking template but with more complex pruning. Instead of "choose an element from a list," you "place a queen in a row" or "fill a cell with a digit" โ and the pruning checks are domain-specific constraints.
- Two queens are on the same diagonal if
row1 - col1 == row2 - col2. - Same anti-diagonal if
row1 + col1 == row2 + col2. Track these in two HashSets โ O(1) conflict check.
Build It Once
Build subsets, permutations, and combination sum โ the three core variants. Every other backtracking problem is one of these with modified pruning.
Use It In Java
List<List<Integer>> result = new ArrayList<>(); List<Integer> path = new ArrayList<>(); result.add(new ArrayList<>(path)); โ always copy! | Idiom | Code | When |
|---|---|---|
| Add to path | path.add(x) | Choose |
| Remove from path | path.removeLast() or path.remove(path.size()-1) | Unchoose |
| Skip duplicates | if (i > start && nums[i] == nums[i-1]) continue; | After sorting โ same level dedup |
| Used array | boolean[] used = new boolean[n]; | Permutations |
| Early termination | if (cands[i] > remain) break; | Sorted input + target constraint |
| N-Queens sets | Set<Integer> cols, diags, antiDiags | O(1) conflict check |
result.add(path) adds a REFERENCE โ as you modify path in later recursive calls, the stored list changes too. You'll end up with all entries pointing to the same empty list. Always copy: result.add(new ArrayList<>(path)).
i = reuse allowed) vs Combination Sum II (i + 1 = no reuse + dedup), mixing up the recursion parameter is the #1 bug. Write a comment next to the recursive call: // i = reuse or // i+1 = no reuse.
Problems To Solve
Backtracking problems cluster into three families: subsets (power set, include/exclude), permutations (arrangments, used[] array), and constraint satisfaction (N-Queens, Sudoku). Master one problem from each family and every other problem is a variant with different pruning.
| Difficulty | Pattern | Problem | Key Insight |
|---|---|---|---|
| Medium | subsets | Subsets โ LC 78 | Collect at every node. start = i + 1. No pruning needed. 2โฟ subsets. |
| Medium | subsets | Subsets II โ LC 90 | Sort + if (i > start && nums[i] == nums[i-1]) continue. Same-level dedup. |
| Medium | permutations | Permutations โ LC 46 | used[] array. Start from 0 every time. Collect at depth n. |
| Medium | combinations | Combination Sum โ LC 39 | Reuse allowed (i). Sort + break when candidate exceeds remainder. |
| Medium | combinations | Combination Sum II โ LC 40 | No reuse (i + 1). Sort + skip duplicates at same level. |
| Hard | constraint | N-Queens โ LC 51 | Place row by row. Prune via cols/diags/antiDiags sets. O(n!) with pruning. |
- "Generate all valid X" โ backtracking.
- The template: define the choice (what goes into the path), the goal (when the path is complete), and the pruning (what to skip).
- Code is always:
path.add(x); recurse(); path.removeLast();. - The hard part is pruning correctly โ sorting the input enables most pruning via
breakorcontinue.
โ See all Backtracking problems with full solutions
โ See Interview Patterns for the complete cheat sheet