Backtracking
Backtracking is DFS on an implicit tree of choices. You mutate a path, recurse, then undo the mutation so the next sibling sees the same path you saw. The three templates here are the ones that show up as “all subsets,” “all orderings,” and “numbers that sum to target.” After the tables, notebook links are other candidates’ families — Job 10454435 still none.
Prev: 02 Trees · Next: 04 Dynamic Programming
Three templates, same undo
Subsets: at each index, the path is already a valid answer — copy it, then try adding a later number. Permutations: a boolean used[] so each value is taken once per path. Combination sum: reuse the same index (unbounded) versus advance to i+1 (0/1). Always copy the list when you record an answer; the live list will be undone.
Subsets — choose / explore / undo
Recognize it from
“Return all subsets / the power set.” “All combinations of size k” (add a cur.size() == k guard). Generate all ways to pick or skip each element. Partition into groups where each group is a subset.
What it actually does
The search tree’s nodes are partial lists. At the start of a call you copy cur into the answer — including the empty list on the first call. Then a for-loop tries each unused later index: append nums[i], recurse with start i+1 (so you never reuse that slot or go backward), then remove the last element. That remove is the undo. Without it, every sibling would see a dirty path.
Simple version is include/exclude at a single index (binary tree of depth n). Interview version is the start-index for-loop (n-ary tree). Same 2ⁿ subsets. The call stack holds the current path; you still need an explicit ArrayList because Java does not give you the path as a stack of values, only as frames plus whatever you mutated.
Simple — read this first
List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> ans = new ArrayList<>();
dfs(0, nums, new ArrayList<>(), ans);
return ans;
}
void dfs(int i, int[] nums, List<Integer> cur, List<List<Integer>> ans) {
if (i == nums.length) {
ans.add(new ArrayList<>(cur)); // copy: cur will be undone on the way up
return;
}
cur.add(nums[i]); // choose
dfs(i + 1, nums, cur, ans); // explore with nums[i]
cur.remove(cur.size() - 1); // undo
dfs(i + 1, nums, cur, ans); // explore without nums[i]
}
Interview — type this
List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> ans = new ArrayList<>();
if (nums == null) return ans;
dfs(0, nums, new ArrayList<>(), ans);
return ans;
}
void dfs(int start, int[] nums, List<Integer> cur, List<List<Integer>> ans) {
ans.add(new ArrayList<>(cur)); // every prefix is a subset, including []
for (int i = start; i < nums.length; i++) {
cur.add(nums[i]); // choose nums[i]
dfs(i + 1, nums, cur, ans); // only later indices — no reuse, no permuting
cur.remove(cur.size() - 1); // undo so the next i sees the old path
}
}
Example 1 — simple
nums = [1, 2]. Every snapshot of cur using the interview start-index template (copy happens at the top of each call).
| Call | start | cur when entering | Copy added | Loop i / action | cur after action |
|---|---|---|---|---|---|
| A | 0 | [] | [] | i=0 choose 1 | [1] |
| B (from A) | 1 | [1] | [1] | i=1 choose 2 | [1, 2] |
| C (from B) | 2 | [1, 2] | [1, 2] | loop empty, return | [1, 2] |
| B resumes | 1 | [1, 2] | — | undo 2 | [1] |
| B | 1 | [1] | — | no more i, return | [1] |
| A resumes | 0 | [1] | — | undo 1 | [] |
| A | 0 | [] | — | i=1 choose 2 | [2] |
| D (from A) | 2 | [2] | [2] | loop empty, return | [2] |
| A resumes | 0 | [2] | — | undo 2, loop done | [] |
Answer: [], [1], [1, 2], [2] — four subsets, copies taken on calls A, B, C, D.
Example 2 — every step
nums = [1, 2, 3]. Every call, the live cur, and the moment a copy is added. Indent = depth.
| Call | start | cur | Copy pushed to ans | Then |
|---|---|---|---|---|
| dfs(0) | 0 | [] | [] | i=0 add 1 |
| dfs(1) | 1 | [1] | [1] | i=1 add 2 |
| dfs(2) | 2 | [1, 2] | [1, 2] | i=2 add 3 |
| dfs(3) | 3 | [1, 2, 3] | [1, 2, 3] | no i, return; undo 3 → [1, 2] |
| dfs(2) resumes | 2 | [1, 2] | — | no more i; undo 2 → [1] |
| dfs(1) resumes | 1 | [1] | — | i=2 add 3 |
| dfs(3) | 3 | [1, 3] | [1, 3] | return; undo 3 → [1]; undo 1 → [] |
| dfs(0) resumes | 0 | [] | — | i=1 add 2 |
| dfs(2) | 2 | [2] | [2] | i=2 add 3 |
| dfs(3) | 3 | [2, 3] | [2, 3] | return; undo 3 → [2]; undo 2 → [] |
| dfs(0) resumes | 0 | [] | — | i=2 add 3 |
| dfs(3) | 3 | [3] | [3] | return; undo 3 → [] |
Answer in copy order: [], [1], [1,2], [1,2,3], [1,3], [2], [2,3], [3]. That is all 2³ subsets. The simple include/exclude version yields the same set in a different order (it only copies at the leaves).
ArrayList of size at most n. Copying it on each node is why time has the extra n factor. Bitmask iteration for (mask = 0; mask < 1<<n; mask++) is the same 2ⁿ answers without recursion; use whichever you can dry-run. Recursion is the one you extend to combination-sum and permutations.
ans.add(cur)without copying — every row of the answer mutates together and ends empty.- Recursing with
iinstead ofi+1, which duplicates numbers and never terminates if you always can pick the same index. - Forgetting undo: the next sibling starts with leftover values.
- Using
cur.remove(nums[i])— that removes by value and breaks on duplicates.
| Problem | Pattern | Links |
|---|---|---|
| Subsets / power set | this template | cheat card · clue |
| Partition / target combination | same choose/undo; extra remaining-sum | Java in ch.05 · cheat card |
Permutations with used[]
Recognize it from
“All permutations of the array.” Next permutation (the single successor — different algo, same ordering idea). “Arrange n distinct items.” Anagrams of a string as permutations of characters.
What it actually does
A permutation is a path of length n that uses each index once. used[i] is true while nums[i] sits on the current path. At depth n you copy cur. At smaller depth you try every index whose bit is still false: mark, append, recurse, then unmark and remove. The call stack is the depth; used[] is the explicit “which values are already in this stack.”
Simple version swaps in the array (the suffix is “still unused”). Interview version uses used[] so the original array stays untouched — easier when you also skip duplicates (if (i > 0 && nums[i]==nums[i-1] && !used[i-1]) continue after sorting). This page’s examples have distinct numbers.
Simple — read this first
List<List<Integer>> permute(int[] nums) {
List<List<Integer>> ans = new ArrayList<>();
dfs(0, nums, ans);
return ans;
}
void dfs(int i, int[] nums, List<List<Integer>> ans) {
if (i == nums.length) {
List<Integer> row = new ArrayList<>();
for (int x : nums) row.add(x);
ans.add(row);
return;
}
for (int j = i; j < nums.length; j++) {
swap(nums, i, j); // choose nums[j] for position i
dfs(i + 1, nums, ans); // suffix i+1.. is the remaining unused
swap(nums, i, j); // undo so the next j sees the old order
}
}
void swap(int[] a, int i, int j) {
int t = a[i]; a[i] = a[j]; a[j] = t;
}
Interview — type this
List<List<Integer>> permute(int[] nums) {
List<List<Integer>> ans = new ArrayList<>();
if (nums == null || nums.length == 0) return ans;
dfs(nums, new boolean[nums.length], new ArrayList<>(), ans);
return ans;
}
void dfs(int[] nums, boolean[] used, List<Integer> cur, List<List<Integer>> ans) {
if (cur.size() == nums.length) {
ans.add(new ArrayList<>(cur)); // copy a finished permutation
return;
}
for (int i = 0; i < nums.length; i++) {
if (used[i]) continue; // already on this path
used[i] = true; // choose index i
cur.add(nums[i]);
dfs(nums, used, cur, ans);
cur.remove(cur.size() - 1); // undo value
used[i] = false; // undo bit — sibling may take i
}
}
Example 1 — simple
nums = [1, 2] with used[]. Bits written as 0/1 for false/true.
| Depth | used | cur | Try i | Action |
|---|---|---|---|---|
| 0 | [0, 0] | [] | i=0 | mark 0, cur=[1] |
| 1 | [1, 0] | [1] | i=0 skip (used) | — |
| 1 | [1, 0] | [1] | i=1 | mark 1, cur=[1, 2] |
| 2 | [1, 1] | [1, 2] | — | size==n, copy [1, 2]; undo → [1], used=[1, 0] |
| 1 | [1, 0] | [1] | loop done | undo → [], used=[0, 0] |
| 0 | [0, 0] | [] | i=1 | mark 1, cur=[2] |
| 1 | [0, 1] | [2] | i=0 | mark 0, cur=[2, 1] |
| 2 | [1, 1] | [2, 1] | — | copy [2, 1]; undo → [2], used=[0, 1] |
| 1 | [0, 1] | [2] | i=1 skip | undo → [], used=[0, 0] |
Answer: [1, 2], [2, 1].
Example 2 — every step
nums = [1, 2, 3]. Every depth shows the three used bits. Copies happen only at depth 3.
| Depth | used [i0,i1,i2] | cur | Event |
|---|---|---|---|
| 0 | [0, 0, 0] | [] | try i=0 → take 1 |
| 1 | [1, 0, 0] | [1] | try i=1 → take 2 |
| 2 | [1, 1, 0] | [1, 2] | try i=2 → take 3 |
| 3 | [1, 1, 1] | [1, 2, 3] | copy [1, 2, 3]; undo 3 |
| 2 | [1, 1, 0] | [1, 2] | no other unused; undo 2 |
| 1 | [1, 0, 0] | [1] | try i=2 → take 3 |
| 2 | [1, 0, 1] | [1, 3] | try i=1 → take 2 |
| 3 | [1, 1, 1] | [1, 3, 2] | copy [1, 3, 2]; undo 2 → used=[1, 0, 1] |
| 2 | [1, 0, 1] | [1, 3] | no other unused; undo 3 → used=[1, 0, 0] |
| 1 | [1, 0, 0] | [1] | loop done; undo 1 → used=[0, 0, 0] |
| 0 | [0, 0, 0] | [] | try i=1 → take 2 |
| 1 | [0, 1, 0] | [2] | take 1 (i=0) |
| 2 | [1, 1, 0] | [2, 1] | take 3 |
| 3 | [1, 1, 1] | [2, 1, 3] | copy [2, 1, 3] |
| 1 | [0, 1, 0] | [2] | take 3 (i=2) |
| 2 | [0, 1, 1] | [2, 3] | take 1 |
| 3 | [1, 1, 1] | [2, 3, 1] | copy [2, 3, 1] |
| 0 | [0, 0, 0] | [] | try i=2 → take 3 |
| 1 | [0, 0, 1] | [3] | take 1 |
| 2 | [1, 0, 1] | [3, 1] | take 2 |
| 3 | [1, 1, 1] | [3, 1, 2] | copy [3, 1, 2] |
| 1 | [0, 0, 1] | [3] | take 2 |
| 2 | [0, 1, 1] | [3, 2] | take 1 |
| 3 | [1, 1, 1] | [3, 2, 1] | copy [3, 2, 1] |
Answer: [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1]. At every depth-3 row the used bits are [1,1,1].
used[] is O(n) and answers “is this index already on the path” in O(1). A HashSet of values also works for distinct numbers and is slower to undo if you are not careful. Swap-in-place uses the array itself as the used-set (left of i is chosen). Next-permutation is not this DFS: it finds the next arrangement in lexicographic order in O(n).
- Leaving
used[i] = trueafter the recursive call — later permutations never reuse that index. - Copying
curby reference. - Looping
i = startas in subsets: that generates combinations, not permutations (order would freeze). - For duplicate values, skipping only
used[i]without the sorted!used[i-1]guard — you emit the same permutation twice.
| Problem | Pattern | Links |
|---|---|---|
| Next permutation | in-place successor, not n! DFS | Java in ch.05 · cheat card |
| All combinations / arrangements | used[] or start-index | clue |
Combination sum — reuse vs 0/1
Recognize it from
“Combinations that add to target.” Coin change that must list the coins, not only the count. Partition an array into a subset with sum T (0/1: each number once). “Unlimited copies of each candidate” vs “each candidate used at most once.”
What it actually does
Same choose/explore/undo as subsets, plus a remaining sum. Unbounded (combination sum): after picking candidates[i] you recurse with the same i, so that value can be picked again. 0/1 (subset sum / combination sum II without duplicates): recurse with i+1 so that slot is spent. Always stop when remain is 0 (copy path) or negative (overshoot — undo).
Simple version is pick-or-skip at index i, and the pick branch passes i unchanged (reuse). Interview version sorts, then a for-loop from start; the recursive call passes i not i+1. Sorting lets you break when candidates[i] > remain. The dry-runs below also show the overshoot branch before that break, so you see the undo.
Simple — read this first
List<List<Integer>> combinationSum(int[] a, int target) {
List<List<Integer>> ans = new ArrayList<>();
dfs(0, target, a, new ArrayList<>(), ans);
return ans;
}
void dfs(int i, int remain, int[] a, List<Integer> cur, List<List<Integer>> ans) {
if (remain == 0) {
ans.add(new ArrayList<>(cur));
return;
}
if (i == a.length || remain < 0) return; // overshoot or no candidates left
cur.add(a[i]);
dfs(i, remain - a[i], a, cur, ans); // reuse i — unbounded knapsack
cur.remove(cur.size() - 1); // undo
dfs(i + 1, remain, a, cur, ans); // skip this value forever (0/1 on this index)
}
Interview — type this
List<List<Integer>> combinationSum(int[] a, int target) {
List<List<Integer>> ans = new ArrayList<>();
if (a == null || a.length == 0) return ans;
Arrays.sort(a); // so we can stop once a[i] > remain
dfs(0, target, a, new ArrayList<>(), ans);
return ans;
}
void dfs(int start, int remain, int[] a, List<Integer> cur, List<List<Integer>> ans) {
if (remain == 0) {
ans.add(new ArrayList<>(cur));
return;
}
for (int i = start; i < a.length; i++) {
if (a[i] > remain) break; // later values are even larger
cur.add(a[i]);
dfs(i, remain - a[i], a, cur, ans); // pass i, not i+1: reuse allowed
cur.remove(cur.size() - 1);
}
}
Example 1 — simple
candidates = [2, 3], target = 5. Interview start-index walk. After [2, 2] remain is 1, so another 2 is refused (overshoot) and that branch backtracks.
| Call | start | remain | cur | Try | Result |
|---|---|---|---|---|---|
| A | 0 | 5 | [] | i=0 take 2 | → B |
| B | 0 | 3 | [2] | i=0 take 2 | → C |
| C | 0 | 1 | [2, 2] | i=0, 2 > 1 | break (would be remain = −1) |
| C | 0 | 1 | [2, 2] | — | return; undo last 2 → [2] |
| B | 0 | 3 | [2] | i=1 take 3 | remain 0 → copy [2, 3] |
| B | 0 | 3 | [2] | undo 3, loop done | undo 2 → [] |
| A | 0 | 5 | [] | i=1 take 3 | → D remain 2 |
| D | 1 | 2 | [3] | i=1, 3 > 2 | break; cannot take 2 again (start=1) |
| D | 1 | 2 | [3] | return, undo 3 | [] |
Answer: [2, 3] only. [3, 2] is the same combination; the start index forbids going backward to 2 after picking 3. The overshoot is [2, 2] then refusing another 2.
Example 2 — every step
candidates = [2, 3, 6, 7], target = 7. Every branch, including those that overshoot (shown as “would go negative,” which the a[i] > remain break catches). Sorted already.
| cur | start | remain | i / value | What happens |
|---|---|---|---|---|
| [] | 0 | 7 | i=0 v=2 | take 2 → remain 5 |
| [2] | 0 | 5 | i=0 v=2 | take 2 → remain 3 |
| [2, 2] | 0 | 3 | i=0 v=2 | take 2 → remain 1 |
| [2, 2, 2] | 0 | 1 | 2 > 1, 3 > 1, 6 > 1, 7 > 1 | overshoot all; backtrack undo 2 → [2, 2] |
| [2, 2] | 0 | 3 | i=1 v=3 | take 3 → remain 0 · copy [2, 2, 3] |
| [2, 2] | 0 | 3 | i=2 v=6 | 6 > 3 break; undo 2 → [2] |
| [2] | 0 | 5 | i=1 v=3 | take 3 → remain 2 |
| [2, 3] | 1 | 2 | 3 > 2, 6 > 2, 7 > 2 | overshoot; undo 3 → [2] |
| [2] | 0 | 5 | i=2 v=6 | 6 > 5 break (7 also larger); undo 2 → [] |
| [] | 0 | 7 | i=1 v=3 | take 3 → remain 4 |
| [3] | 1 | 4 | i=1 v=3 | take 3 → remain 1 |
| [3, 3] | 1 | 1 | 3 > 1 | overshoot; undo 3 → [3] |
| [3] | 1 | 4 | i=2 v=6 | 6 > 4 break; undo 3 → [] |
| [] | 0 | 7 | i=2 v=6 | take 6 → remain 1 |
| [6] | 2 | 1 | 6 > 1, 7 > 1 | overshoot; undo 6 → [] |
| [] | 0 | 7 | i=3 v=7 | take 7 → remain 0 · copy [7] |
| [] | 0 | 7 | done | — |
Answer: [2, 2, 3] and [7]. The branches that die are [2,2,2] (remain 1), [2,3] (remain 2), [3,3] (remain 1), and [6] (remain 1). Each of those undoes and tries the next sibling.
If the spec were 0/1 (each index once), the pick would call dfs(i+1, remain - a[i]). Then [2, 2, 3] would be illegal unless 2 appeared twice in the input array.
- Passing
i+1on an unbounded problem — you miss[2,2,3]. - Passing
ion a 0/1 / “each number once” problem — infinite recursion on a positive candidate, or illegal repeats. - Not copying the path when remain hits 0.
- Continuing the loop after remain goes negative instead of returning / breaking.
- Starting the next combination at 0 every time, which emits permutations of the same combo (
[2,3]and[3,2]).
| Problem | Pattern | Links |
|---|---|---|
| Partition / target combination | 0/1 or unbounded remaining-sum DFS | Java in ch.05 · cheat card · clue |
| Subsets (no target) | same start-index, copy every node | cheat card |