BacktrackingRecursionPermutationsCombinationsSubsetsConstraint SatisfactionPruning

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.

50 min read10 sections
01

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.

Recursion Tree — Subsets of [1, 2, 3]text
                        []
                      /    \
                   [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.

02

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

PatternWhen to UseKey Difference
BacktrackingGenerate ALL valid solutions; constraint satisfaction; small nExplores the full decision tree with pruning. Output is a list of solutions.
DFS (Graph/Tree)Traverse a graph or tree; visit all nodesDFS is the traversal mechanism. Backtracking uses DFS but adds choose/unchoose and pruning.
Dynamic ProgrammingOptimal value (min/max/count) with overlapping subproblemsDP caches results to avoid recomputation. Backtracking doesn't cache — it enumerates.
GreedyLocally optimal choice provably leads to global optimumGreedy commits to one choice and never backtracks. Backtracking tries all choices.
Brute ForceTry every possibility without any pruningBacktracking 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.

03

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.

1

Brute Force — Generate and Filter

O(n! × n) time · O(n! × n) space

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.

Brute Force — Generate All, Filter Latertypescript
// 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.
2

Backtracking — Build Incrementally with Pruning

O(n! × n) time · O(n) extra space (recursion stack)

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.

Backtracking — Permutationstypescript
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?

1

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

2

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.

3

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!."

04

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

Universal Backtracking Templatetypescript
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.

Subsets Templatetypescript
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.

Permutations Templatetypescript
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.

Combinations Templatetypescript
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.

Constraint Satisfaction Templatetypescript
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).

05

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.

VariationKey ModificationClassic Example
Subsets (power set)Collect result at every node, not just leaves. Start index prevents duplicates.Subsets, Subsets II
PermutationsNo start index — any unused element can go next. Track used elements.Permutations, Permutations II
CombinationsLike subsets but only collect when current.length === k. Prune when not enough elements remain.Combinations, Combination Sum
With duplicatesSort 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 reusePass i (not i+1) as the next start index — same element can be chosen again.Combination Sum (unlimited), Letter Combinations
Grid searchChoices = 4 directions. Mark cells visited. Unmark on backtrack.Word Search, Unique Paths III
Constraint satisfactionCheck constraints before choosing. Multiple constraint sets (cols, diags, etc.).N-Queens, Sudoku Solver
PartitioningTry 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.

Subsets II — Handling Duplicatestypescript
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.

Combination Sum — Unlimited Reusetypescript
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.

Word Search — Grid Backtrackingtypescript
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.
06

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]

Permutations — Step-by-Step Tracetext
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

N-Queens n=4 — Step-by-Step Tracetext
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 0BLOCKED (col 0 used)
         Try col 1BLOCKED (diag2: 0+0=0, 1+1=2wait, not blocked)
         Actually col 1diag1: 1-1=0BLOCKED (same main diagonal)
         Try col 2cols 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 0BLOCKED (col)
           Try col 1diag2: 2+1=3BLOCKED
           Try col 2BLOCKED (col)
           Try col 3diag1: 2-3=-1BLOCKED
           ALL BLOCKEDbacktrack 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 1all checks pass
  Q . . .
  . . . Q
  . Q . .     cols={0,1,3}, diag1={0,-2,1}, diag2={0,4,3}
      Row 3: Try col 0BLOCKED (col)
             Try col 1BLOCKED (col)
             Try col 2diag1: 3-2=1BLOCKED
             Try col 3BLOCKED (col)
             ALL BLOCKEDbacktrack

    Unchoose col 1. Try col 2diag1: 2-2=0BLOCKED
    ALL BLOCKEDbacktrack to Row 1
  
  Unchoose col 3ALL options for Row 1 exhaustedbacktrack 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.
07

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.

1

LC 78. Subsets

Medium

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.

2

LC 46. Permutations

Medium

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.

3

LC 39. Combination Sum

Medium

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.

4

LC 90. Subsets II

Medium

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.

5

LC 79. Word Search

Medium

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.

6

LC 131. Palindrome Partitioning

Medium

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.

7

LC 51. N-Queens

Hard

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.

08

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.

09

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

1

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.

2

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.

3

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.

4

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.

5

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

10

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

When to Use Backtracking — Decision Treetext
Does the problem ask to GENERATE or LIST all valid solutions?
├── NOProbably not backtracking.
Need optimal value? → DP or Greedy
Need count only? → DP (often)
Need one solution? → Backtracking with early return, or DP
└── YESIs the input size small (n15-20)?
          ├── NOBacktracking may TLE. Look for DP or math shortcuts.
          └── YESBacktracking! 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?
├── NOStandard template
└── YESSort first. Skip: if (i > start && nums[i] === nums[i-1]) continue

Can elements be reused?
├── NOrecurse with i + 1 (or use 'used' array)
└── YESrecurse 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.