Backtracking — The Complete Guide
Master the Backtracking pattern from first principles. Learn to recognize 'generate all', 'find all valid', and constraint-satisfaction problems instantly, evolve brute-force into pruned search, and confidently solve every backtracking interview question.
Table of Contents
Intuition from Scratch
Why does this pattern exist?
Imagine you're in a maze. You walk forward, hit a dead end, walk back to the last fork, and try a different path. You keep doing this — exploring, hitting walls, undoing your last step, trying the next option — until you find the exit or exhaust every path. That's backtracking.
In programming, backtracking is a systematic way to explore all possible solutions by building them one choice at a time. After each choice, you check: "Is this still valid? Could it lead to a solution?" If yes, keep going. If no, undo the last choice (backtrack) and try the next option. This "try → check → undo" cycle is the heart of the pattern.
The pattern exists because some problems require you to find all valid configurations or any one valid configuration that satisfies a set of constraints. There's no formula or shortcut — you have to search. But backtracking searches smartly by pruning branches that can't possibly lead to a valid answer, avoiding the need to enumerate every single possibility.
The Recursion Tree Mental Model
Every backtracking problem can be visualized as a tree. Each node is a partial solution. Each edge is a choice. Leaves are complete solutions (or dead ends). Backtracking is just DFS on this tree with pruning.
[] / \ [1] [] ← choose or skip 1 / \ / \ [1,2] [1] [2] [] ← choose or skip 2 / \ / \ / \ / \ [1,2,3] [1,2] [1,3] [1] [2,3] [2] [3] [] ← choose or skip 3 Each leaf is a valid subset. Total leaves = 2^n = 8. Backtracking visits this tree via DFS, collecting results at leaves.
Analogies to Build Intuition
Solving a Jigsaw Puzzle
You pick a piece, try to place it. If it fits, move to the next piece. If it doesn't, put it back and try another. You never force a piece — you backtrack and try alternatives. That's exactly how backtracking explores the solution space.
Cracking a Combination Lock
A 3-digit lock has 1000 combinations (10 × 10 × 10). Brute force tries all 1000. But if you know the first digit is wrong after testing it, you skip all 100 combinations starting with that digit. That's pruning — the key optimization in backtracking.
Navigating a Maze
At each fork, you pick a direction. If you hit a dead end, you walk back to the last fork and try the other path. You mark visited paths to avoid loops. This explore-and-retreat strategy is backtracking in its purest form.
What kind of problems does it solve?
Backtracking is your go-to when:
- You need to generate all permutations, combinations, or subsets
- You need to find all valid configurations satisfying constraints (N-Queens, Sudoku)
- You need to partition data into groups with specific properties
- You need to search for a path in a grid or graph with constraints
- The problem says "find all" or "generate all" valid answers
- The solution space is exponential but can be pruned with constraints
The core insight
Backtracking = DFS on the decision tree + pruning. You build solutions one choice at a time, and the moment a partial solution violates a constraint, you abandon that entire branch. The power isn't in the search — it's in what you DON'T search. Good pruning can turn an exponential search into something practical.
Pattern Recognition
Recognizing when backtracking is the right approach is the most important skill. Here are the signals to look for and the traps to avoid.
Keywords & Signals in the Problem Statement
Use Backtracking when you see...
- ✅"Generate all permutations / combinations / subsets"
- ✅"Find all valid configurations" (N-Queens, Sudoku)
- ✅"Partition into K subsets with equal sum"
- ✅"Word search" in a grid — path with constraints
- ✅"Letter combinations of a phone number"
- ✅"Palindrome partitioning" — split into all valid palindromes
- ✅"Generate all valid parentheses"
- ✅"Combination sum" — find all combos that sum to target
- ✅Small input size (n ≤ 15-20) with exponential output
- ✅Constraint satisfaction — place items following rules
Don't use Backtracking when...
- ❌You need a single optimal value (min/max) — consider DP or Greedy
- ❌The problem has overlapping subproblems — use DP with memoization
- ❌You need the count of solutions, not the solutions themselves — often DP
- ❌Input size is large (n > 20) and output isn't exponential — too slow
- ❌The problem is about contiguous subarrays — use Sliding Window
- ❌A greedy choice provably works — no need to explore all options
Backtracking vs. Similar Patterns
| Pattern | When to Use | Key Difference |
|---|---|---|
| Backtracking | Generate ALL valid solutions; constraint satisfaction; small n | Explores the full decision tree with pruning. Output is a list of solutions. |
| DFS (Graph/Tree) | Traverse a graph or tree; visit all nodes | DFS is the traversal mechanism. Backtracking uses DFS but adds choose/unchoose and pruning. |
| Dynamic Programming | Optimal value (min/max/count) with overlapping subproblems | DP caches results to avoid recomputation. Backtracking doesn't cache — it enumerates. |
| Greedy | Locally optimal choice provably leads to global optimum | Greedy commits to one choice and never backtracks. Backtracking tries all choices. |
| Brute Force | Try every possibility without any pruning | Backtracking IS brute force + pruning. The pruning is what makes it practical. |
The Backtracking vs. DP Decision
This is the most common confusion. Here's the rule of thumb:
- Need ALL solutions listed? → Backtracking (you must enumerate them)
- Need the COUNT of solutions? → Often DP (you don't need to list them)
- Need the OPTIMAL value? → DP or Greedy (backtracking is overkill)
- Overlapping subproblems? → DP. No overlap? → Backtracking
The backtracking litmus test
Ask yourself: "Does the problem require me to enumerate or list all valid answers?" If yes, backtracking. If the problem only asks for a count or an optimal value, check if DP or greedy can solve it first. Backtracking is the "last resort" search — powerful but expensive.
Brute Force → Optimized Thinking
Let's walk through the thinking evolution using a concrete example: Permutations — given an array of distinct integers, return all possible permutations.
Brute Force — Generate and Filter
Generate every possible arrangement of n elements (n! total), then filter out invalid ones. For permutations of distinct elements, every arrangement is valid — but this approach doesn't scale and doesn't generalize to constrained problems.
// Conceptual: generate all n^n arrangements, keep valid permutations // This is wasteful — we generate invalid arrangements and discard them. // For [1,2,3]: generates 27 arrangements (3^3), keeps only 6 (3!). // The ratio gets worse as n grows.
Backtracking — Build Incrementally with Pruning
Build the permutation one element at a time. At each position, try every unused element. If an element is already used, skip it (prune). When the permutation is complete, record it. Then undo the last choice and try the next option.
function permute(nums: number[]): number[][] { const result: number[][] = []; const current: number[] = []; const used = new Set<number>(); function backtrack(): void { // Base case: permutation is complete if (current.length === nums.length) { result.push([...current]); // copy! return; } for (const num of nums) { // Prune: skip already-used elements if (used.has(num)) continue; // Choose current.push(num); used.add(num); // Explore backtrack(); // Unchoose (backtrack) current.pop(); used.delete(num); } } backtrack(); return result; } // The "choose → explore → unchoose" pattern is the backbone. // Pruning (skip used elements) prevents invalid branches.
Why Does Backtracking Work Better Than Brute Force?
It never builds invalid partial solutions
Brute force generates all n^n arrangements and filters. Backtracking only explores valid branches — if an element is already used, that entire subtree is skipped. For permutations, this reduces n^n to n! (e.g., 27 → 6 for n=3).
Pruning cuts exponential branches early
In constrained problems (N-Queens, Sudoku), pruning is even more powerful. If placing a queen in column 2 creates a conflict, you skip ALL arrangements that start with that placement — potentially millions of branches eliminated by one check.
The 'unchoose' step enables reuse
By undoing each choice after exploring its subtree, you restore the state for the next option. This means you only need O(n) space for the current path, not O(n!) space for all paths simultaneously.
The thinking process matters more than the code
In interviews, walk through this progression: "The brute force generates all possible arrangements and filters — that's O(n^n). But I can build the solution incrementally: at each position, I only try unused elements. This is backtracking — I explore, and if a choice leads nowhere, I undo it and try the next. The pruning reduces the search space from n^n to n!."
Core Templates
Backtracking problems follow a universal template with small variations. Memorize this structure — every problem is a variation of it.
The Universal Backtracking Template
function solve(input): Result[] { const result: Result[] = []; function backtrack(current, startOrState): void { // 1. BASE CASE — is current a complete solution? if (isComplete(current)) { result.push(copy(current)); // always copy! return; } // 2. ITERATE over choices available at this step for (const choice of getChoices(startOrState)) { // 3. PRUNE — skip invalid choices early if (!isValid(choice, current)) continue; // 4. CHOOSE — make the choice current.add(choice); markUsed(choice); // 5. EXPLORE — recurse to the next decision backtrack(current, nextState); // 6. UNCHOOSE — undo the choice (backtrack) current.remove(choice); unmarkUsed(choice); } } backtrack(initialCurrent, initialState); return result; } // The 6 steps: Base → Iterate → Prune → Choose → Explore → Unchoose // This template covers permutations, combinations, subsets, // constraint satisfaction, and grid search problems.
Template 1: Subsets (Include / Exclude)
For each element, you have two choices: include it or skip it. This generates all 2^n subsets. The "start index" parameter prevents duplicates by only considering elements after the current position.
function subsets(nums: number[]): number[][] { const result: number[][] = []; const current: number[] = []; function backtrack(start: number): void { // Every partial solution is a valid subset result.push([...current]); for (let i = start; i < nums.length; i++) { current.push(nums[i]); // choose backtrack(i + 1); // explore (i+1 = no reuse) current.pop(); // unchoose } } backtrack(0); return result; } // Time: O(2^n × n) Space: O(n) recursion depth // Key: start index prevents [1,2] and [2,1] from both appearing // Every node in the recursion tree is a valid subset (not just leaves)
Template 2: Permutations (Use Each Once)
Every element must appear exactly once. Use a "used" set or boolean array to track which elements are already in the current permutation.
function permute(nums: number[]): number[][] { const result: number[][] = []; const current: number[] = []; const used = new Array(nums.length).fill(false); function backtrack(): void { if (current.length === nums.length) { result.push([...current]); return; } for (let i = 0; i < nums.length; i++) { if (used[i]) continue; // prune: already used used[i] = true; // choose current.push(nums[i]); backtrack(); // explore current.pop(); // unchoose used[i] = false; } } backtrack(); return result; } // Time: O(n! × n) Space: O(n) // Key difference from subsets: no start index (any unused element // can go at any position), but we track used elements.
Template 3: Combinations (Choose K from N)
Like subsets, but only collect results when the current path has exactly k elements. Use a start index to avoid duplicates.
function combine(n: number, k: number): number[][] { const result: number[][] = []; const current: number[] = []; function backtrack(start: number): void { if (current.length === k) { result.push([...current]); return; } // Pruning: need (k - current.length) more elements, // so don't start beyond n - (k - current.length) + 1 const remaining = k - current.length; for (let i = start; i <= n - remaining + 1; i++) { current.push(i); // choose backtrack(i + 1); // explore current.pop(); // unchoose } } backtrack(1); return result; } // Time: O(C(n,k) × k) Space: O(k) // Key: the pruning condition (i <= n - remaining + 1) skips // branches where there aren't enough elements left to fill k slots.
Template 4: Constraint Satisfaction (N-Queens Style)
Place items one at a time (row by row, cell by cell). At each step, check all constraints. If any constraint is violated, prune. If all items are placed, record the solution.
function solveNQueens(n: number): string[][] { const result: string[][] = []; const cols = new Set<number>(); const diag1 = new Set<number>(); // row - col const diag2 = new Set<number>(); // row + col const board: string[][] = Array.from({ length: n }, () => new Array(n).fill('.') ); function backtrack(row: number): void { if (row === n) { result.push(board.map(r => r.join(''))); return; } for (let col = 0; col < n; col++) { // Prune: check all constraints if (cols.has(col) || diag1.has(row - col) || diag2.has(row + col)) { continue; } // Choose board[row][col] = 'Q'; cols.add(col); diag1.add(row - col); diag2.add(row + col); // Explore backtrack(row + 1); // Unchoose board[row][col] = '.'; cols.delete(col); diag1.delete(row - col); diag2.delete(row + col); } } backtrack(0); return result; } // Time: O(n!) roughly Space: O(n²) for the board // Key: three sets track column, main diagonal, and anti-diagonal // conflicts. Checking all three is O(1) per candidate.
Which template to pick?
Ask yourself: (1) Do I need all subsets? → Template 1 (start index, collect at every node). (2) Do I need all permutations? → Template 2 (used array, collect at leaves). (3) Do I need combinations of size k? → Template 3 (start index + length check). (4) Am I placing items with constraints? → Template 4 (constraint checks before choosing).
Variations & Sub-Patterns
Backtracking isn't a single trick — it's a family of techniques. Here are the most common variations and how the approach changes for each.
| Variation | Key Modification | Classic Example |
|---|---|---|
| Subsets (power set) | Collect result at every node, not just leaves. Start index prevents duplicates. | Subsets, Subsets II |
| Permutations | No start index — any unused element can go next. Track used elements. | Permutations, Permutations II |
| Combinations | Like subsets but only collect when current.length === k. Prune when not enough elements remain. | Combinations, Combination Sum |
| With duplicates | Sort input first. Skip nums[i] if nums[i] === nums[i-1] and i-1 wasn't used at this level. | Subsets II, Permutations II, Combination Sum II |
| Unlimited reuse | Pass i (not i+1) as the next start index — same element can be chosen again. | Combination Sum (unlimited), Letter Combinations |
| Grid search | Choices = 4 directions. Mark cells visited. Unmark on backtrack. | Word Search, Unique Paths III |
| Constraint satisfaction | Check constraints before choosing. Multiple constraint sets (cols, diags, etc.). | N-Queens, Sudoku Solver |
| Partitioning | Try every possible split point. Validate each partition before recursing. | Palindrome Partitioning, Partition to K Equal Sum Subsets |
Problems mentioned above
Deep Dive: Handling Duplicates
When the input contains duplicates (e.g., [1, 2, 2]), you get duplicate subsets/permutations unless you handle them. The trick: sort the input, then skip an element if it's the same as the previous one at the same decision level.
function subsetsWithDup(nums: number[]): number[][] { nums.sort((a, b) => a - b); // Step 1: sort! const result: number[][] = []; const current: number[] = []; function backtrack(start: number): void { result.push([...current]); for (let i = start; i < nums.length; i++) { // Skip duplicates at the SAME decision level if (i > start && nums[i] === nums[i - 1]) continue; current.push(nums[i]); backtrack(i + 1); current.pop(); } } backtrack(0); return result; } // Key: "i > start" means we only skip if this is NOT the first // choice at this level. The first occurrence is always allowed. // [1, 2, 2]: we allow [1,2(first),2(second)] but skip the branch // where we start with the second 2 at the same level as the first.
Deep Dive: Combination Sum with Unlimited Reuse
When elements can be reused, the only change is passing i instead of i + 1 as the start index. This allows the same element to be chosen again.
function combinationSum(candidates: number[], target: number): number[][] { const result: number[][] = []; const current: number[] = []; function backtrack(start: number, remaining: number): void { if (remaining === 0) { result.push([...current]); return; } if (remaining < 0) return; // prune: overshot for (let i = start; i < candidates.length; i++) { current.push(candidates[i]); backtrack(i, remaining - candidates[i]); // i, not i+1! current.pop(); } } backtrack(0, target); return result; } // Time: depends on target and candidates — exponential in worst case // Key: backtrack(i, ...) allows reuse. backtrack(i+1, ...) would not. // Sorting candidates and pruning when candidates[i] > remaining // can significantly speed this up.
Deep Dive: Grid Search — Word Search
Grid backtracking uses 4-directional movement as choices. Mark cells as visited to avoid cycles. Unmark on backtrack.
function exist(board: string[][], word: string): boolean { const rows = board.length, cols = board[0].length; function backtrack(r: number, c: number, idx: number): boolean { if (idx === word.length) return true; // found! // Prune: out of bounds, wrong char, or already visited if (r < 0 || r >= rows || c < 0 || c >= cols) return false; if (board[r][c] !== word[idx]) return false; // Choose: mark as visited const temp = board[r][c]; board[r][c] = '#'; // Explore: try all 4 directions const found = backtrack(r + 1, c, idx + 1) || backtrack(r - 1, c, idx + 1) || backtrack(r, c + 1, idx + 1) || backtrack(r, c - 1, idx + 1); // Unchoose: restore the cell board[r][c] = temp; return found; } for (let r = 0; r < rows; r++) { for (let c = 0; c < cols; c++) { if (backtrack(r, c, 0)) return true; } } return false; } // Time: O(m × n × 4^L) where L = word length // Key: modify the board in-place to mark visited (saves space). // Restore it on backtrack. This is a common interview trick.
Visual Walkthrough
Let's trace through two key backtracking problems step by step to build deep intuition for the choose → explore → unchoose cycle.
Permutations of [1, 2, 3]
Input: [1, 2, 3] current = [], used = {} Call backtrack(): ├── Choose 1: current=[1], used={1} │ ├── Choose 2: current=[1,2], used={1,2} │ │ ├── Choose 3: current=[1,2,3] → COMPLETE ✓ → result=[[1,2,3]] │ │ └── Unchoose 3: current=[1,2] │ ├── Unchoose 2: current=[1] │ ├── Choose 3: current=[1,3], used={1,3} │ │ ├── Choose 2: current=[1,3,2] → COMPLETE ✓ → result=[[1,2,3],[1,3,2]] │ │ └── Unchoose 2: current=[1,3] │ ├── Unchoose 3: current=[1] ├── Unchoose 1: current=[] ├── Choose 2: current=[2], used={2} │ ├── Choose 1: current=[2,1], used={1,2} │ │ ├── Choose 3: current=[2,1,3] → COMPLETE ✓ │ │ └── Unchoose 3 │ ├── Unchoose 1 │ ├── Choose 3: current=[2,3], used={2,3} │ │ ├── Choose 1: current=[2,3,1] → COMPLETE ✓ │ │ └── Unchoose 1 │ ├── Unchoose 3 ├── Unchoose 2: current=[] ├── Choose 3: current=[3], used={3} │ ├── ... (similar pattern) │ └── Produces [3,1,2] and [3,2,1] Final result: [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]] Total: 3! = 6 permutations Key observations: - Each "Choose" pushes to current and marks used - Each "Unchoose" pops from current and unmarks used - The used set prevents picking the same element twice - Every leaf (complete permutation) is added to result
N-Queens (n=4) — Constraint Satisfaction
Board: 4×4. Place 4 queens, one per row, no conflicts. Constraints: no two queens share a column, main diagonal, or anti-diagonal. Row 0: Try col 0 Q . . . cols={0}, diag1={0}, diag2={0} Row 1: Try col 0 → BLOCKED (col 0 used) Try col 1 → BLOCKED (diag2: 0+0=0, 1+1=2 — wait, not blocked) Actually col 1 → diag1: 1-1=0 → BLOCKED (same main diagonal) Try col 2 → cols ok, diag1: 1-2=-1 ok, diag2: 1+2=3 ok ✓ Q . . . . . Q . cols={0,2}, diag1={0,-1}, diag2={0,3} Row 2: Try col 0 → BLOCKED (col) Try col 1 → diag2: 2+1=3 → BLOCKED Try col 2 → BLOCKED (col) Try col 3 → diag1: 2-3=-1 → BLOCKED ALL BLOCKED → backtrack to Row 1 Row 1: Unchoose col 2. Try col 3. Q . . . . . . Q cols={0,3}, diag1={0,-2}, diag2={0,4} Row 2: Try col 1 → all checks pass ✓ Q . . . . . . Q . Q . . cols={0,1,3}, diag1={0,-2,1}, diag2={0,4,3} Row 3: Try col 0 → BLOCKED (col) Try col 1 → BLOCKED (col) Try col 2 → diag1: 3-2=1 → BLOCKED Try col 3 → BLOCKED (col) ALL BLOCKED → backtrack Unchoose col 1. Try col 2 → diag1: 2-2=0 → BLOCKED ALL BLOCKED → backtrack to Row 1 Unchoose col 3 → ALL options for Row 1 exhausted → backtrack to Row 0 Row 0: Unchoose col 0. Try col 1. . Q . . cols={1}, diag1={-1}, diag2={1} Row 1: Try col 3 → ✓ . Q . . . . . Q Row 2: Try col 0 → ✓ . Q . . . . . Q Q . . . Row 3: Try col 2 → ✓ → SOLUTION FOUND! ★ . Q . . . . . Q Q . . . . . Q . This is one of two solutions for n=4. Pruning eliminated most of the 4^4 = 256 possible placements.
Practice Problems
These 7 problems are carefully ordered from easy to hard. Each one teaches a different aspect of the Backtracking pattern. Solve them in order.
LC 78. Subsets
Why this pattern:
The purest backtracking problem. No constraints, no duplicates — just generate all 2^n subsets using the include/exclude template. Every node in the recursion tree is a valid result.
Key idea: Use a start index to avoid duplicates. At each step, add the current path to results (every partial solution is a valid subset). Then try including each remaining element and recurse with start = i + 1.
LC 46. Permutations
Why this pattern:
Classic permutation template. Every element must appear exactly once. Use a 'used' set to track which elements are in the current permutation. Collect results only at leaves (when current.length === n).
Key idea: No start index needed — any unused element can go at any position. Loop through all elements, skip used ones, choose/explore/unchoose. The used set is the key difference from subsets.
LC 39. Combination Sum
Why this pattern:
Combinations with unlimited reuse. Pass i (not i+1) to allow reusing the same element. Prune when the remaining target goes negative.
Key idea: Sort candidates for better pruning. At each step, try each candidate starting from index i. Subtract from target. If target === 0, record. If target < 0, prune. Passing i (not i+1) allows reuse.
LC 90. Subsets II
Why this pattern:
Subsets with duplicates. Sort the input first, then skip nums[i] if it equals nums[i-1] at the same decision level (i > start). This is the standard duplicate-handling technique.
Key idea: Sort the array. In the loop, add the check: if (i > start && nums[i] === nums[i-1]) continue. This skips duplicate branches at the same level while still allowing duplicates at different depths.
LC 79. Word Search
Why this pattern:
Grid backtracking. Choices = 4 directions. Mark cells visited by modifying the board in-place. Unmark on backtrack. Prune when the current cell doesn't match the expected character.
Key idea: For each cell matching word[0], start backtracking. Mark visited by setting board[r][c] = '#'. Try all 4 directions. Restore the cell on backtrack. Return true as soon as any path completes the word.
LC 131. Palindrome Partitioning
Why this pattern:
Partitioning backtracking. At each position, try every possible cut point. If the substring from start to cut is a palindrome, include it and recurse on the remainder.
Key idea: At index start, try every end from start to s.length-1. If s[start..end] is a palindrome, add it to current and recurse with start = end + 1. When start === s.length, the entire string is partitioned — record the result.
LC 51. N-Queens
Why this pattern:
Constraint satisfaction. Place one queen per row. For each row, try every column. Prune using three sets: columns, main diagonals (row-col), and anti-diagonals (row+col).
Key idea: Process row by row. For each column in the current row, check if it conflicts with any placed queen (same column, same diagonal). If valid, place and recurse to the next row. The three-set approach makes conflict checking O(1).
Practice strategy
For each problem: (1) Draw the recursion tree for a small input before coding. (2) Identify: what are the choices? What's the base case? What's the pruning condition? (3) After solving, classify it: subsets, permutations, combinations, grid search, or constraint satisfaction.
Common Mistakes
These are the traps that catch people on Backtracking problems. Learn them here so you don't learn them in an interview.
Forgetting to copy the current path
You push 'current' directly into the result array. But current is mutated throughout the recursion — by the end, every entry in result points to the same empty array.
✅Always push a copy: result.push([...current]) or result.push(current.slice()). This captures the state at that moment, not a reference to the mutable array.
Forgetting to unchoose (undo the choice)
You add an element to current or mark it as used, but forget to undo it after the recursive call returns. The state 'leaks' into sibling branches, producing wrong results.
✅Every 'choose' must have a matching 'unchoose' after the recursive call. Use the pattern: push → recurse → pop. set(true) → recurse → set(false). Make it mechanical.
Generating duplicate results
With input [1, 2, 2], you get duplicate subsets like [1,2] appearing twice because both 2s are treated as distinct.
✅Sort the input first. Then skip duplicates at the same decision level: if (i > start && nums[i] === nums[i-1]) continue. The 'i > start' condition is critical — it allows the first occurrence but skips subsequent duplicates at the same level.
Not pruning early enough
You let the recursion go deep into invalid branches before checking constraints. For N-Queens, checking conflicts only at the end (when all queens are placed) is exponentially slower than checking after each placement.
✅Check constraints as early as possible — ideally before making the choice (in the loop). The earlier you prune, the more branches you skip. Move your 'isValid' check to the top of the loop, not the base case.
Confusing start index vs used array
Using a start index for permutations (wrong — you need all elements, not just later ones) or a used array for subsets (works but unnecessarily complex).
✅Subsets/Combinations: use a start index (order doesn't matter, avoid [2,1] when you have [1,2]). Permutations: use a used array (order matters, [1,2] and [2,1] are different). Match the mechanism to the problem type.
Infinite recursion from missing base case
You forget the base case or the base case condition is wrong. The recursion never terminates, causing a stack overflow.
✅Always write the base case FIRST. For subsets: no explicit base needed (loop naturally ends). For permutations: current.length === n. For combinations: current.length === k. For grid search: index === word.length.
Interview Strategy
Knowing the pattern is half the battle. Here's how to present it in an interview setting to maximize your score.
The 5-Step Interview Flow
Clarify & Classify
'I need to generate ALL valid solutions, not just count them or find the optimal one. The input size is small (n ≤ 15). This is a backtracking problem.' — Classify it as subsets, permutations, combinations, grid search, or constraint satisfaction.
Define the Decision Space
'At each step, my choices are [list them]. The base case is [when I stop]. The constraints are [what makes a choice invalid].' — Drawing the first 2-3 levels of the recursion tree on the whiteboard is extremely effective.
Identify Pruning Opportunities
'I can prune when [constraint is violated]. For duplicates, I'll sort and skip. For combinations, I'll stop when not enough elements remain.' — Pruning is what separates a good answer from a great one.
Code the Template
Write the choose → explore → unchoose structure. Keep it clean: the backtrack function should be short. Put constraint checks in a helper or inline them clearly. Always copy the result.
Trace & Analyze Complexity
Trace through a small example (n=3) showing the recursion tree. State complexity: 'O(2^n × n) for subsets, O(n! × n) for permutations, O(C(n,k) × k) for combinations. Space is O(n) for the recursion stack.'
Common Follow-Up Questions
| Follow-Up | How to Handle |
|---|---|
| "What if there are duplicates in the input?" | Sort the input. Skip nums[i] when nums[i] === nums[i-1] and i > start (for subsets/combos) or when the previous duplicate wasn't used at this level (for permutations). |
| "Can you optimize the pruning?" | Sort the input so you can break early (e.g., if candidates[i] > remaining target, all subsequent candidates are also too large). Use sets for O(1) constraint checking (N-Queens diagonals). |
| "What's the time complexity?" | Subsets: O(2^n × n). Permutations: O(n! × n). Combinations C(n,k): O(C(n,k) × k). N-Queens: O(n!). The ×n factor is for copying each solution. Be precise about this. |
| "Can you do it iteratively?" | Yes — use an explicit stack to simulate the recursion. But recursive backtracking is cleaner and preferred in interviews. Only switch to iterative if asked. |
| "What if I only need ONE solution, not all?" | Return true/false from backtrack. When you find a solution, return true immediately. The caller checks the return value and stops exploring. This short-circuits the search. |
| "Can you use memoization here?" | If the problem has overlapping subproblems (same state reached via different paths), yes — that's DP. But classic backtracking problems (permutations, N-Queens) don't have overlap, so memoization doesn't help. |
The meta-skill
Interviewers aren't just testing if you can write the template. They're testing if you can (1) identify the decision space, (2) define effective pruning, and (3) handle edge cases like duplicates. Drawing the recursion tree on the whiteboard is the single most effective thing you can do — it shows you understand the structure, not just the code.
Summary & Cheat Sheet
Pin this section. Review it before mock interviews and contests.
Quick Revision Cheat Sheet
Core idea: Build solutions one choice at a time. If a choice violates constraints, undo it (backtrack) and try the next. DFS on the decision tree + pruning.
The template: Base case → Iterate choices → Prune → Choose → Explore (recurse) → Unchoose. Every backtracking problem follows this 6-step structure.
Subsets: Start index, collect at every node. 2^n results. No used array needed.
Permutations: Used array, collect at leaves only. n! results. No start index — any unused element can go next.
Combinations: Start index + length check. C(n,k) results. Prune when not enough elements remain.
Handling duplicates: Sort input. Skip nums[i] if nums[i] === nums[i-1] and i > start. The first occurrence is always allowed.
Unlimited reuse: Pass i (not i+1) as start index. Same element can be chosen again.
Grid search: 4-directional choices. Mark visited in-place. Restore on backtrack.
Constraint satisfaction: Check constraints BEFORE choosing (in the loop). Use sets for O(1) checks. Prune early.
Always copy: result.push([...current]) — never push the mutable array directly.
Mental trigger: "Generate all", "find all valid", "permutations", "combinations", "subsets", small n ≤ 15-20 → Backtracking
Decision Flowchart
Does the problem ask to GENERATE or LIST all valid solutions? ├── NO → Probably not backtracking. │ Need optimal value? → DP or Greedy │ Need count only? → DP (often) │ Need one solution? → Backtracking with early return, or DP └── YES → Is the input size small (n ≤ 15-20)? ├── NO → Backtracking may TLE. Look for DP or math shortcuts. └── YES → Backtracking! What type? ├── All subsets? → Template 1 (start index, collect at every node) ├── All permutations? → Template 2 (used array, collect at leaves) ├── All combinations? → Template 3 (start index + length k) ├── Grid path search? → Template: 4-dir, mark visited, restore └── Constraint puzzle? → Template 4 (check constraints before choosing) Duplicates in input? ├── NO → Standard template └── YES → Sort first. Skip: if (i > start && nums[i] === nums[i-1]) continue Can elements be reused? ├── NO → recurse with i + 1 (or use 'used' array) └── YES → recurse with i (same element can be chosen again)
One last thing
Backtracking is the pattern that makes "hard" problems feel approachable. N-Queens, Sudoku Solver, Word Search — they all follow the same choose → explore → unchoose template. The difference between problems is just what the choices are, what the constraints are, and when to collect results. Master the template, and the variations become mechanical. Solve the 7 practice problems in Section 07, and you'll have that intuition locked in.