Recursion and Backtracking Master Recap — Templates, Pattern Cues, and Top 30 Problems
Advertisement
Problem Statement
This is the recap article for the recursion and backtracking series. It collects every reusable template, every pattern cue, and every duplicate-handling rule into one place so you can review quickly before an interview.
Constraints:
- All templates assume the choose-explore-unchoose discipline
- All recursive calls must respect either a start index (subsets, combinations) or a used array (permutations)
- Pruning must never drop valid solutions
- Snapshots are mandatory before storing a state
Input: Any "generate all", "find all valid", "count arrangements" problem
Output: A working backtracking implementation in roughly 15 linesWhy This Problem Matters
Backtracking is one of the highest-leverage topics in DSA interviews. Amazon, Google, Meta, and Microsoft together ask backtracking problems in roughly one of every three onsite loops. The templates do not change — only the validity check and the pruning rule do — which means a single recap session can cover a large slice of your interview surface area.
A well-prepared candidate solves any of LC 39, 40, 46, 47, 78, 90, 51, 79, 17, 22, 131 in under 12 minutes once the pattern is recognized. The bottleneck is almost never the code; it is recognizing which template applies and which pruning rule to layer on top.
This recap is structured for spaced repetition. Skim the templates the day before an interview, walk through one problem from the top-30 list, and you will be ready.
The Core Insight
Every backtracking problem has the same skeleton: a base case that records or returns the answer, an optional pruning step, a loop over candidate choices, and the choose-explore-unchoose triple. Different problems differ only in three places — what defines completeness, what counts as a candidate, and what pruning is correctness-preserving.
Visual Dry Run
| Cue | Pattern | Template |
|---|---|---|
| "generate all subsets" | Subsets / Power Set | bt(start, current) |
| "generate all permutations" | Permutations | bt(current) with used array |
| "find all combinations summing to..." | Combination Sum | bt(start, current, remaining) |
| "place N pieces" | N-Queens | bt(row, col_set, diag_sets) |
| "fill grid" | Sudoku | bt(cell_index, constraints) |
| "find all paths" | Word Search / Maze | bt(r, c) with mark/unmark |
| "partition into valid groups" | Palindrome Partition | bt(start, parts) |
Solution (Optimal)
class Solution:
# 1. Subsets — include or exclude each element
def subsets(self, nums):
result = []
def bt(start, current):
result.append(current[:])
for i in range(start, len(nums)):
current.append(nums[i])
bt(i + 1, current)
current.pop()
bt(0, [])
return result
# 2. Permutations — used array, every element a candidate at every slot
def permute(self, nums):
result, used = [], [False] * len(nums)
def bt(current):
if len(current) == len(nums):
result.append(current[:])
return
for i in range(len(nums)):
if used[i]:
continue
used[i] = True
current.append(nums[i])
bt(current)
current.pop()
used[i] = False
bt([])
return result
# 3. Combination Sum — sort, prune by remaining, allow reuse via i not i+1
def combinationSum(self, candidates, target):
candidates.sort()
result = []
def bt(start, current, remaining):
if remaining == 0:
result.append(current[:])
return
for i in range(start, len(candidates)):
if candidates[i] > remaining:
break # sorted, so all later candidates are too large
current.append(candidates[i])
bt(i, current, remaining - candidates[i]) # i for reuse
current.pop()
bt(0, [], target)
return result
# 4. N-Queens — column and diagonal sets prune O(n!) to manageable
def nQueens(self, n):
result = []
cols, d1, d2 = set(), set(), set()
board = [['.'] * n for _ in range(n)]
def bt(r):
if r == n:
result.append([''.join(row) for row in board])
return
for c in range(n):
if c in cols or (r - c) in d1 or (r + c) in d2:
continue
board[r][c] = 'Q'
cols.add(c); d1.add(r - c); d2.add(r + c)
bt(r + 1)
board[r][c] = '.'
cols.discard(c); d1.discard(r - c); d2.discard(r + c)
bt(0)
return result// 1. Subsets
var subsets = function(nums) {
const result = [];
const bt = (start, current) => {
result.push([...current]);
for (let i = start; i < nums.length; i++) {
current.push(nums[i]);
bt(i + 1, current);
current.pop();
}
};
bt(0, []);
return result;
};
// 2. Permutations
var permute = function(nums) {
const result = [];
const used = new Array(nums.length).fill(false);
const bt = (current) => {
if (current.length === nums.length) {
result.push([...current]);
return;
}
for (let i = 0; i < nums.length; i++) {
if (used[i]) continue;
used[i] = true;
current.push(nums[i]);
bt(current);
current.pop();
used[i] = false;
}
};
bt([]);
return result;
};
// 3. Combination Sum
var combinationSum = function(candidates, target) {
candidates.sort((a, b) => a - b);
const result = [];
const bt = (start, current, remaining) => {
if (remaining === 0) {
result.push([...current]);
return;
}
for (let i = start; i < candidates.length; i++) {
if (candidates[i] > remaining) break;
current.push(candidates[i]);
bt(i, current, remaining - candidates[i]);
current.pop();
}
};
bt(0, [], target);
return result;
};Time: Pattern-dependent — see complexity table below Space: O(depth) recursion plus O(output_size) for storing results
Duplicate Handling Rules
| Situation | Rule |
|---|---|
| Subsets with duplicates | Sort then if i > start and nums[i] == nums[i-1] continue |
| Permutations with duplicates | Sort then if i > 0 and nums[i] == nums[i-1] and not used[i-1] continue |
| Combination Sum (no reuse) | Pass i + 1 to the recursive call |
| Combination Sum (reuse allowed) | Pass i to the recursive call |
| Bucket / partition equal subsets | Track a per-level seen set of bucket values |
Complexity Quick Reference
| Problem | Time | Note |
|---|---|---|
| Subsets, n elements | O(2^n * n) | Snapshot copy each |
| Permutations, n elements | O(n! * n) | Snapshot copy each |
| Combinations C(n, k) | O(C(n, k) * k) | k is selection size |
| N-Queens n by n | O(n!) before pruning | Diagonal sets cut deeply |
| Sudoku 9 by 9 | O(9^81) worst | Constraint prop makes it fast |
| Word Search length L on m by n grid | O(m * n * 4^L) | DFS with mark |
| Combination Sum target T | O(T^(T / min)) | With sort + prune |
Common Mistakes
- Forgetting to copy the current state when recording — every later mutation poisons the result
- Skipping the unchoose step — sibling branches inherit dirty state
- Using start index when a used array is needed (or vice versa)
- Applying duplicate-skip without sorting first — adjacency is required
- Confusing reuse (
i) versus single-use (i + 1) in combination problems - Off-by-one errors in the pruning bound — always verify on a small case
Interview Tips
- Always state the four-part skeleton before writing code
- Draw the decision tree for n=3 or n=4 to verify your understanding
- Articulate the pruning rule and prove it preserves correctness
- Mention the bitmask alternative when n is small (less than 22)
- Estimate complexity in terms of branching factor and depth, not just big-O
Follow-up Questions
- When does DP replace backtracking? Hint: counting or boolean queries with overlapping subproblems.
- How do you parallelize backtracking? Hint: distribute root-level branches across workers.
- How do you stream solutions instead of storing all of them? Hint: yield in Python or callback in JavaScript.
- How does iterative deepening DFS apply? Hint: bounded depth then increase, useful for game-tree search.
- When is BFS-on-states better than DFS? Hint: shortest-path enumeration like LC 301 Remove Invalid Parentheses.
Top 30 Backtracking LeetCode Problems
| LC | Problem | Pattern |
|---|---|---|
| 17 | Letter Combinations of a Phone Number | Recursive tree |
| 22 | Generate Parentheses | Counter constraints |
| 37 | Sudoku Solver | Cell-by-cell constraints |
| 39 | Combination Sum | Reuse with start index |
| 40 | Combination Sum II | No reuse plus dup-skip |
| 46 | Permutations | Used array |
| 47 | Permutations II | Used plus dup-skip |
| 51 | N-Queens | Row plus three sets |
| 52 | N-Queens II | Bitmask count |
| 77 | Combinations | k of n with prune |
| 78 | Subsets | Include or exclude |
| 79 | Word Search | Grid DFS plus unmark |
| 90 | Subsets II | Sort plus dup-skip |
| 93 | Restore IP Addresses | Segment validity |
| 131 | Palindrome Partitioning | Precomputed palindrome plus bt |
| 140 | Word Break II | Memoized backtracking |
| 212 | Word Search II | Trie plus grid DFS |
| 216 | Combination Sum III | k from 1 to 9 |
| 282 | Expression Add Operators | Operator insertion |
| 301 | Remove Invalid Parentheses | BFS minimum plus DFS all |
| 306 | Additive Number | Partition by additive rule |
| 320 | Generalized Abbreviation | Char-level include or exclude |
| 351 | Android Unlock Patterns | Grid DFS with skip rules |
| 401 | Binary Watch | Popcount combinations |
| 416 | Partition Equal Subset Sum | DP not bt |
| 425 | Word Squares | Trie-guided bt |
| 473 | Matchsticks to Square | k-bucket partition |
| 526 | Beautiful Arrangement | Position-number validity |
| 698 | Partition K Equal Subsets | Bucket dedup |
| 784 | Letter Case Permutation | Toggle-case tree |
Key Takeaways
- Backtracking has only a handful of templates — recognition matters more than memorization
- Subsets uses start index, permutations uses used array, combinations uses both
- Always snapshot before storing; always undo before exiting the loop iteration
- Sort first when applying any duplicate-skip rule
- Reuse vs no-reuse in combinations is the difference between passing
iandi + 1 - Pruning must be correctness-preserving — never drop a valid path
- The four-part skeleton (base, prune, loop, choose-explore-unchoose) covers every problem in this series
Advertisement