Algorithms

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.

Foundations ยท Array ยท HashMap ยท DFS ยท Sorting ยท Binary Search ยท Two Pointers ยท Sliding Window ยท Dynamic Prog. ยท Backtracking ยท Greedy
01
Section One ยท Foundation

What is Backtracking?

Decision tree for subsets of 3 [] [1] [2] [3] [1,2] [1,3] [2,3] [1,2,3] โ‘  Choose: add element โ‘ก Explore: recurse deeper โ‘ข Unchoose: remove (undo)

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.

Analogy: Navigating a maze. At each fork, you pick a direction (choose), walk until you hit a dead end or find the exit (explore), then walk back to the fork and try the other direction (unchoose). You never revisit a path you've already fully explored. Pruning is when you can see the dead end ahead and skip the path without walking all the way.
02
Section Two ยท Mental Model

How It Thinks

"Generate all valid X" โ€” the problem asks for all configurations, not just one optimal answer
โ–ถ
Backtracking: DFS the decision tree, collect every valid leaf (or intermediate) node. Unlike DP which optimises, backtracking enumerates.
Choose โ†’ Explore โ†’ Unchoose: add to current path, recurse, remove from current path
โ–ถ
The path is reused across all branches โ€” it's mutable state. The "unchoose" step restores it to the state before the choice, enabling the next branch to start clean.
The start parameter controls which elements are available
โ–ถ
For subsets/combinations: start = i + 1 โ†’ never revisit earlier elements โ†’ no duplicates. For permutations: start = 0 + used[] โ†’ any unused element can be placed.
Pruning: skip branches that can't lead to valid solutions
โ–ถ
Dramatically reduces the search space. E.g., in N-Queens: skip columns/diagonals already attacked. In combination sum: skip if remaining sum is negative. Sorting the input enables early termination.
Duplicate elements in input โ†’ duplicate results
โ–ถ
Sort first. Then skip: 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.
Copy the path when adding to result โ€” don't add the reference
โ–ถ
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.
03
Section Three ยท Template

The Universal Backtracking Template

Pseudocode
function backtrack(result, path, choices, start): if isGoal(path): // base case โ€” reached a valid solution result.add(copy(path)) // COPY, not reference return for i = start to choices.length - 1: if shouldPrune(i): continue // prune invalid branches path.add(choices[i]) // โ‘  CHOOSE backtrack(result, path, choices, nextStart(i)) // โ‘ก EXPLORE path.removeLast() // โ‘ข UNCHOOSE
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
Every backtracking problem is the same template.:
  • 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.
04
Section Four ยท Subsets

Subsets (Power Set)

Subsets (LC 78)
Given a set of distinct integers, return all possible subsets.
Java
List<List<Integer>> subsets(int[] nums) { List<List<Integer>> result = new ArrayList<>(); backtrack(result, new ArrayList<>(), nums, 0); return result; } void backtrack(List<List<Integer>> res, List<Integer> path, int[] nums, int start) { res.add(new ArrayList<>(path)); // collect at EVERY node for (int i = start; i < nums.length; i++) { path.add(nums[i]); // choose backtrack(res, path, nums, i + 1); // explore (i+1 โ†’ no reuse) path.removeLast(); // unchoose } }
Subsets II โ€” With Duplicates (LC 90)
Input may have duplicates. Return all unique subsets.
Key change: sort + skip duplicates at same level
Arrays.sort(nums); // sort first! // Inside the loop, add this skip: if (i > start && nums[i] == nums[i - 1]) continue; // "i > start" means: skip only at the SAME level, not deeper
Subsets = collect at every node. Unlike combinations (collect at leaves of depth k) or permutations (collect at leaves of depth n), subsets add the current path to the result at the BEGINNING of every recursion call โ€” before the loop.
05
Section Five ยท Permutations

Permutations

Permutations (LC 46)
Given distinct integers, return all possible permutations.
Java
List<List<Integer>> permute(int[] nums) { List<List<Integer>> result = new ArrayList<>(); backtrack(result, new ArrayList<>(), nums, new boolean[nums.length]); return result; } void backtrack(List<List<Integer>> res, List<Integer> path, int[] nums, boolean[] used) { if (path.size() == nums.length) { // all placed โ†’ complete perm res.add(new ArrayList<>(path)); return; } for (int i = 0; i < nums.length; i++) { // start from 0 every time if (used[i]) continue; // skip already-used used[i] = true; // choose path.add(nums[i]); backtrack(res, path, nums, used); // explore path.removeLast(); // unchoose used[i] = false; } }
Subsets / Combinations

start = i + 1

Never revisit earlier elements. Order doesn't matter: 2 and 1 are the same subset. Loop from start forward.

Permutations

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.

06
Section Six ยท Combinations

Combinations & Combination Sum

Combinations (LC 77)
Return all combinations of k numbers from 1..n.
Java
void backtrack(List<List<Integer>> res, List<Integer> path, int n, int k, int start) { if (path.size() == k) { // reached target size res.add(new ArrayList<>(path)); return; } for (int i = start; i <= n - (k - path.size()) + 1; i++) { // prune! path.add(i); backtrack(res, path, n, k, i + 1); path.removeLast(); } }
Combination Sum (LC 39)
Find all unique combinations that sum to target. Candidates may be reused.
Java โ€” key difference: recurse with i (not i+1) for reuse
void backtrack(List<List<Integer>> res, List<Integer> path, int[] cands, int remain, int start) { if (remain == 0) { res.add(new ArrayList<>(path)); return; } for (int i = start; i < cands.length; i++) { if (cands[i] > remain) break; // prune (array sorted) path.add(cands[i]); backtrack(res, path, cands, remain - cands[i], i); // i, not i+1! path.removeLast(); } }
Common Mistake: Combination Sum allows reuse (i), 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.
07
Section Seven ยท Constraint Problems

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.

N-Queens (LC 51)
Place n queens on an nร—n chessboard so no two attack each other.
Approach
// Place one queen per row. For each row, try each column. // Prune: skip if column, diagonal, or anti-diagonal is attacked. // Track with 3 sets: cols, diags (row-col), antiDiags (row+col). void solve(int row, int n, List<String> board, Set cols, Set diags, Set antiDiags) { if (row == n) { result.add(new ArrayList<>(board)); return; } for (int col = 0; col < n; col++) { if (cols.contains(col) || diags.contains(row-col) || antiDiags.contains(row+col)) continue; // prune // choose cols.add(col); diags.add(row-col); antiDiags.add(row+col); board.set(row, placeQueen(col, n)); solve(row + 1, ...); // explore // unchoose cols.remove(col); diags.remove(row-col); antiDiags.remove(row+col); board.set(row, emptyRow(n)); } }
N-Queens pruning trick:
  • 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.
08
Section Eight ยท Implementation

Build It Once

Build subsets, permutations, and combination sum โ€” the three core variants. Every other backtracking problem is one of these with modified pruning.

09
Section Nine ยท Java Reference

Use It In Java

IN JAVA โ€” Backtracking Idioms
Result type List<List<Integer>> result = new ArrayList<>();
Path (mutable) List<Integer> path = new ArrayList<>();
Copy to result 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
โš  Gotcha: 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)).
โš  Gotcha: For Combination Sum (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.
10
Section Ten ยท Practice

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.
Interview Pattern:
  • "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 break or continue.

โ†’ See all Backtracking problems with full solutions
โ†’ See Interview Patterns for the complete cheat sheet