Common Backtracking Patterns

What are the common backtracking patterns?

TL;DR

Constraints

Quick Reference

PatternTime ComplexitySpacePruning StrategyClassic Problems
Permutations (no dups)O(n! * n)O(n)Skip used elements via visited[]Permutations (LC 46)
Permutations (with dups)O(n! * n)O(n)Sort + skip nums[i] == nums[i-1] if prev unusedPermutations II (LC 47)
CombinationsO(C(n,k) * k)O(k)Start index prevents revisiting; prune if remaining < neededCombinations (LC 77)
Combination Sum (reuse)O(n^(t/min))O(t/min)Skip candidates > remaining targetCombination Sum (LC 39)
Combination Sum (no reuse)O(2^n * n)O(n)Sort + skip duplicates + start indexCombination Sum II (LC 40)
Subsets (no dups)O(2^n * n)O(n)Start index; collect at every nodeSubsets (LC 78)
Subsets (with dups)O(2^n * n)O(n)Sort + skip nums[i] == nums[i-1]Subsets II (LC 90)
N-QueensO(n!)O(n)Column, diagonal, anti-diagonal setsN-Queens (LC 51)
Sudoku SolverO(9^m), m = blanksO(m)Row/col/box constraint setsSudoku Solver (LC 37)
Word Search (grid)O(m*n * 4^L)O(L)Bounds check + visited cell markingWord Search (LC 79)
Palindrome PartitioningO(n * 2^n)O(n)Prune non-palindrome prefixes earlyPalindrome Partitioning (LC 131)

Decision Tree

START: Do you need to enumerate ALL valid solutions (not just count or optimize)?
├── YES: Is there a constraint that eliminates large branches early?
│   ├── YES → Backtracking with pruning (this unit)
│   └── NO → Backtracking still works but may be slow; check if problem size allows exponential time
├── NO: Do subproblems overlap (same state reachable via different paths)?
│   ├── YES → Dynamic Programming
│   └── NO ↓
├── Need shortest path / minimum steps in state space?
│   ├── YES → BFS (breadth-first search)
│   └── NO ↓
├── Can you prove greedy choice property?
│   ├── YES → Greedy algorithm
│   └── NO ↓
└── DEFAULT → Backtracking with pruning is the safe fallback for constraint satisfaction

Step-by-Step Guide

1. Identify the backtracking structure

Determine what constitutes a "choice" at each step, what the "path" (partial solution) looks like, and when you've reached a complete solution (base case). Draw the decision tree on paper first. [src1]

Three key concepts:
- PATH:    choices already made (the partial solution so far)
- CHOICES: options available at the current step
- BASE:    condition where path is a complete valid solution

Verify: Can you draw a tree where each level represents one choice? If yes, backtracking applies.

2. Write the general template

Every backtracking solution follows this skeleton. Customize the three marked sections. [src1]

def backtrack(path, choices):
    if is_complete(path):          # BASE CASE
        result.append(path[:])     # save a copy
        return
    for choice in choices:
        if not is_valid(choice):   # PRUNE
            continue
        path.append(choice)        # CHOOSE
        backtrack(path, next_choices)  # EXPLORE
        path.pop()                 # UNCHOOSE (undo)

Verify: Every append has a matching pop, and the base case copies the path.

3. Add pruning to reduce search space

Without pruning, you explore every branch. Add constraints that skip invalid choices before recursing. [src3]

# Combination sum: prune when remaining target < 0
def backtrack(path, start, remaining):
    if remaining == 0:
        result.append(path[:])
        return
    for i in range(start, len(candidates)):
        if candidates[i] > remaining:  # PRUNE
            break
        path.append(candidates[i])
        backtrack(path, i, remaining - candidates[i])
        path.pop()

Verify: Sort input first when using comparison-based pruning.

4. Handle duplicates

Sort the input, then skip consecutive duplicates at the same decision level. [src2]

candidates.sort()
for i in range(start, len(candidates)):
    if i > start and candidates[i] == candidates[i - 1]:
        continue  # skip duplicate at same level
    # ... choose, explore, unchoose

Verify: Output should contain no duplicate subsets/combinations. Test with [1, 1, 2].

Code Examples

Python: Subsets (Power Set)

# Input:  nums = [1, 2, 3]
# Output: [[], [1], [1,2], [1,2,3], [1,3], [2], [2,3], [3]]

def subsets(nums):
    result = []
    def backtrack(start, path):
        result.append(path[:])      # collect at every node
        for i in range(start, len(nums)):
            path.append(nums[i])    # choose
            backtrack(i + 1, path)  # explore (i+1 prevents reuse)
            path.pop()              # unchoose
    backtrack(0, [])
    return result

Python: N-Queens

# Input:  n = 4
# Output: all valid board configurations as lists of queen positions

def solve_n_queens(n):
    result, cols, diags, anti = [], set(), set(), set()
    def backtrack(row, queens):
        if row == n:
            result.append(queens[:])
            return
        for col in range(n):
            if col in cols or (row-col) in diags or (row+col) in anti:
                continue              # prune: conflict detected
            cols.add(col); diags.add(row-col); anti.add(row+col)
            backtrack(row + 1, queens + [col])
            cols.remove(col); diags.remove(row-col); anti.remove(row+col)
    backtrack(0, [])
    return result

JavaScript: Permutations

// Input:  nums = [1, 2, 3]
// Output: [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

function permute(nums) {
  const result = [];
  function backtrack(path, used) {
    if (path.length === nums.length) { result.push([...path]); return; }
    for (let i = 0; i < nums.length; i++) {
      if (used[i]) continue;         // skip already-used elements
      used[i] = true; path.push(nums[i]);  // choose
      backtrack(path, used);                // explore
      path.pop(); used[i] = false;          // unchoose
    }
  }
  backtrack([], new Array(nums.length).fill(false));
  return result;
}

JavaScript: Combination Sum

// Input:  candidates = [2,3,6,7], target = 7
// Output: [[2,2,3],[7]]

function combinationSum(candidates, target) {
  candidates.sort((a, b) => a - b);
  const result = [];
  function backtrack(start, path, remaining) {
    if (remaining === 0) { result.push([...path]); return; }
    for (let i = start; i < candidates.length; i++) {
      if (candidates[i] > remaining) break;  // prune
      path.push(candidates[i]);
      backtrack(i, path, remaining - candidates[i]); // i, not i+1 (reuse)
      path.pop();
    }
  }
  backtrack(0, [], target);
  return result;
}

Java: Sudoku Solver

Full script: SudokuSolver.java (45 lines)

// Input:  9x9 board with '.' for empty cells
// Output: solved board in-place

public void solveSudoku(char[][] board) { solve(board); }
private boolean solve(char[][] board) {
    for (int r = 0; r < 9; r++)
        for (int c = 0; c < 9; c++)
            if (board[r][c] == '.') {
                for (char d = '1'; d <= '9'; d++) {
                    if (isValid(board, r, c, d)) {
                        board[r][c] = d;       // choose
                        if (solve(board)) return true;  // explore
                        board[r][c] = '.';     // unchoose
                    }
                }
                return false;  // no valid digit: trigger backtrack
            }
    return true;  // all cells filled
}

Java: Palindrome Partitioning

// Input:  s = "aab"
// Output: [["a","a","b"],["aa","b"]]

public List<List<String>> partition(String s) {
    List<List<String>> result = new ArrayList<>();
    backtrack(s, 0, new ArrayList<>(), result);
    return result;
}
void backtrack(String s, int start, List<String> path, List<List<String>> result) {
    if (start == s.length()) { result.add(new ArrayList<>(path)); return; }
    for (int end = start + 1; end <= s.length(); end++) {
        String sub = s.substring(start, end);
        if (!isPalindrome(sub)) continue;  // prune non-palindromes
        path.add(sub);
        backtrack(s, end, path, result);
        path.remove(path.size() - 1);
    }
}

Anti-Patterns

Wrong: Forgetting to undo state changes

# BAD -- missing path.pop() after recursion
def backtrack(path, start):
    result.append(path[:])
    for i in range(start, len(nums)):
        path.append(nums[i])
        backtrack(path, i + 1)
        # forgot path.pop() -- path grows forever, all results wrong

Correct: Always undo after recursion

# GOOD -- symmetric choose/unchoose
def backtrack(path, start):
    result.append(path[:])
    for i in range(start, len(nums)):
        path.append(nums[i])     # choose
        backtrack(path, i + 1)   # explore
        path.pop()               # unchoose -- MUST match the append

Wrong: Storing reference instead of copy

# BAD -- appending the same list object, not a snapshot
def backtrack(path, start):
    if len(path) == k:
        result.append(path)       # all entries point to same list!

Correct: Copy the path before storing

# GOOD -- append a shallow copy
def backtrack(path, start):
    if len(path) == k:
        result.append(path[:])    # path[:] or list(path) creates a copy

Wrong: No pruning on sorted candidates

# BAD -- tries every candidate even when sum exceeds target
for i in range(start, len(candidates)):
    path.append(candidates[i])
    backtrack(path, i, remaining - candidates[i])
    path.pop()

Correct: Break early when candidate exceeds remaining

# GOOD -- sort first, break when too large
candidates.sort()
for i in range(start, len(candidates)):
    if candidates[i] > remaining:
        break                     # all subsequent also too large
    path.append(candidates[i])
    backtrack(path, i, remaining - candidates[i])
    path.pop()

Wrong: Not handling duplicates at same level

# BAD -- produces duplicate subsets for input [1, 1, 2]
for i in range(start, len(nums)):
    path.append(nums[i])
    backtrack(path, i + 1)
    path.pop()

Correct: Skip duplicates at same level after sorting

# GOOD -- skip nums[i] if equals nums[i-1] at same decision level
nums.sort()
for i in range(start, len(nums)):
    if i > start and nums[i] == nums[i - 1]:
        continue                  # skip duplicate at same tree level
    path.append(nums[i])
    backtrack(path, i + 1)
    path.pop()

Common Pitfalls

When to Use / When Not to Use

Use WhenDon't Use WhenUse Instead
Need ALL valid solutions (enumerate every permutation, combination, subset)Need only the COUNT of solutionsDynamic Programming
Constraint satisfaction (N-Queens, Sudoku, crossword)Problem has overlapping subproblems with optimal substructureDynamic Programming (memoization)
Search space can be pruned significantly by constraintsNeed shortest path or minimum stepsBFS (breadth-first search)
Input size small enough for exponential time (n <= ~20 for subsets)Input size is large (n > 25 for subsets)DP, math formulas, or meet-in-the-middle
Need to generate actual solutions, not just existence/countState space is infinite or unboundedIterative deepening or bounded search
Grid/board puzzles with local constraints (word search, maze)Graph has no dead ends to prunePlain DFS suffices
Decision tree is finite and boundedGreedy choice property is provableGreedy algorithm

Important Caveats