DSA Notes · chapter 03 · Java Live Code

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

03

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).

choose / explore / undo on [1, 2]
Callstartcur when enteringCopy addedLoop i / actioncur after action
A0[][]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 resumes1[1, 2]—undo 2[1]
B1[1]—no more i, return[1]
A resumes0[1]—undo 1[]
A0[]—i=1 choose 2[2]
D (from A)2[2][2]loop empty, return[2]
A resumes0[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.

Interview dfs(start) on [1, 2, 3] — 8 calls, 8 copies
CallstartcurCopy pushed to ansThen
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) resumes2[1, 2]—no more i; undo 2 → [1]
dfs(1) resumes1[1]—i=2 add 3
dfs(3)3[1, 3][1, 3]return; undo 3 → [1]; undo 1 → []
dfs(0) resumes0[]—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) resumes0[]—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).

Time O(n · 2ⁿ) Space O(n) extra
Why this DS The live path is one 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.
Common mistakes
  • ans.add(cur) without copying — every row of the answer mutates together and ends empty.
  • Recursing with i instead of i+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.
ProblemPatternLinks
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.

Every used bit and the two answers
DepthusedcurTry iAction
0[0, 0][]i=0mark 0, cur=[1]
1[1, 0][1]i=0 skip (used)—
1[1, 0][1]i=1mark 1, cur=[1, 2]
2[1, 1][1, 2]—size==n, copy [1, 2]; undo → [1], used=[1, 0]
1[1, 0][1]loop doneundo → [], used=[0, 0]
0[0, 0][]i=1mark 1, cur=[2]
1[0, 1][2]i=0mark 0, cur=[2, 1]
2[1, 1][2, 1]—copy [2, 1]; undo → [2], used=[0, 1]
1[0, 1][2]i=1 skipundo → [], 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.

used[] at each depth — 3! = 6 permutations
Depthused [i0,i1,i2]curEvent
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].

Time O(n · n!) Space O(n) extra
Why this DS 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).
Common mistakes
  • Leaving used[i] = true after the recursive call — later permutations never reuse that index.
  • Copying cur by reference.
  • Looping i = start as 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.
ProblemPatternLinks
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.

dfs(start, remain, cur) — reuse i
CallstartremaincurTryResult
A05[]i=0 take 2→ B
B03[2]i=0 take 2→ C
C01[2, 2]i=0, 2 > 1break (would be remain = −1)
C01[2, 2]—return; undo last 2 → [2]
B03[2]i=1 take 3remain 0 → copy [2, 3]
B03[2]undo 3, loop doneundo 2 → []
A05[]i=1 take 3→ D remain 2
D12[3]i=1, 3 > 2break; cannot take 2 again (start=1)
D12[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.

Full start-index tree. Copies only when remain hits 0.
curstartremaini / valueWhat happens
[]07i=0 v=2take 2 → remain 5
[2]05i=0 v=2take 2 → remain 3
[2, 2]03i=0 v=2take 2 → remain 1
[2, 2, 2]012 > 1, 3 > 1, 6 > 1, 7 > 1overshoot all; backtrack undo 2 → [2, 2]
[2, 2]03i=1 v=3take 3 → remain 0 · copy [2, 2, 3]
[2, 2]03i=2 v=66 > 3 break; undo 2 → [2]
[2]05i=1 v=3take 3 → remain 2
[2, 3]123 > 2, 6 > 2, 7 > 2overshoot; undo 3 → [2]
[2]05i=2 v=66 > 5 break (7 also larger); undo 2 → []
[]07i=1 v=3take 3 → remain 4
[3]14i=1 v=3take 3 → remain 1
[3, 3]113 > 1overshoot; undo 3 → [3]
[3]14i=2 v=66 > 4 break; undo 3 → []
[]07i=2 v=6take 6 → remain 1
[6]216 > 1, 7 > 1overshoot; undo 6 → []
[]07i=3 v=7take 7 → remain 0 · copy [7]
[]07done—

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.

Time O(nT/min) worst Space O(T/min) depth
Why this DS The path is an explicit list; remain is an integer on the stack. DP coin-change counts ways or min coins in O(n·target) but does not list the combinations unless you also store parents. Backtracking lists them and dies exponentially if target is large. Sort + break is optional speed, not required for correctness.
Common mistakes
  • Passing i+1 on an unbounded problem — you miss [2,2,3].
  • Passing i on 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]).
ProblemPatternLinks
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
See also Next permutation is not the n! listing — it is the in-place successor (ch.05 / ch.24). Partition-to-target is this chapter’s combination DFS with a 0/1 skip. There is no Round 4 leftover-hour card required for these three templates.