Common Backtracking Patterns

Type: Software Reference Confidence: 0.95 Sources: 7 Verified: 2026-02-24 Freshness: 2026-02-24

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

Related Units