Patterns

Backtracking Patterns — Complete Guide

Master every backtracking problem on LeetCode with one unified template. This guide covers the decision-tree mental model, the choose/explore/unchoose pattern, and how to apply it across permutations, combinations, subsets, constraint satisfaction (N-Queens, Sudoku), and when to reach for DP instead.

12 min read|

Backtracking Patterns

Complete Guide

What Is Backtracking

Backtracking is a systematic trial-and-error algorithm: you build solution candidates incrementally and abandon (backtrack) a partial candidate as soon as you determine it cannot possibly lead to a valid complete solution. This aggressive pruning is what makes backtracking efficient — rather than generating all possible sequences and filtering, you cut off entire subtrees of the search space the moment a constraint is violated.

The mental model that unlocks every backtracking problem is the decision tree. Each node in the tree represents a partial solution (a path you have built so far). Each edge represents a choice you make (an element you add to the path). Backtracking is simply depth-first search on this implicit decision tree. You go deep, hit a dead end or find a solution, then undo the last choice and try the next branch.

Understanding what triggers a backtrack is critical. You backtrack when: you have built a complete solution (record it, then backtrack to find more), or when continuing from the current state can never yield a valid solution (pruning). The efficiency of a backtracking solution is almost entirely determined by how aggressively and correctly you prune.

  • Incrementally builds candidates — adds one element at a time to the current path
  • Prunes aggressively — abandons paths the moment a constraint is violated
  • DFS on the decision tree — explores one full branch before trying alternatives
  • Explores all valid paths — guaranteed to find every solution, not just one
  • Time complexity is exponential in the worst case — O(n!) for permutations, O(2^n) for subsets
  • Space complexity is O(n) for the recursion stack (depth of decision tree)

The Universal Template

Every backtracking problem follows the same three-step skeleton: choose, explore, unchoose. In code, this translates to a recursive function backtrack(path, choices) that first checks if path represents a complete valid solution (if so, add to results), then iterates over available choices, makes a choice (mutates path and/or the choices set), recurses, and undoes the choice after the recursion returns. This undo step — the "unchoose" — is what restores state so the next iteration starts clean.

The template handles all cases uniformly because the only parts that change between problems are: what constitutes a valid "choice" at each step (the pruning condition), and what constitutes a "complete solution" (the base case). The recursion structure, the choose/recurse/unchoose loop, and the result collection are identical across permutations, combinations, subsets, and constraint satisfaction.

A common implementation error is forgetting the unchoose step or placing it in the wrong location. The unchoose must happen after the recursive call returns, not before it. If you push an element onto path before recursing, you must pop it after recursing. If you mark a cell as visited before recursing, you must unmark it after. Consistent symmetric choice/unchoose pairs are the hallmark of correct backtracking code.

💡

Every Backtracking Problem Uses the Same Skeleton

Every backtracking problem follows the same skeleton: choose, explore, unchoose. The only thing that changes between problems is what constitutes a valid choice and what constitutes a complete solution. If you internalize this template, you can reduce any backtracking problem to filling in two conditions rather than redesigning the algorithm from scratch.

Permutations Pattern

Permutations are the prototypical backtracking problem: order matters, every element is used exactly once, and there are n! results for an input of size n. The key insight is that at each level of the decision tree, you choose from elements that are not yet in the current path. The standard implementation uses a boolean "used" array (or a set) to track which elements are already on the path, and at each recursive call iterates over all elements, skipping those already used.

The decision tree for permutations of [1, 2, 3] has 3 choices at the root, 2 choices at depth 1, and 1 choice at depth 2 — matching the n! count. The base case fires when the path length equals the input length, at which point you have a complete permutation. This "path.length === n" base case is the most common form for permutation-style problems.

For problems with duplicate elements (LeetCode 47 — Permutations II), sort the input first and add a pruning condition: skip element i if nums[i] === nums[i-1] and nums[i-1] is not currently in use. This prevents generating duplicate permutations by ensuring that among duplicate values, you always use the leftmost available copy first.

  1. 1Step 1 — Sort the input if duplicates are possible
  2. 2Step 2 — Initialize a used[] boolean array of length n and an empty path
  3. 3Step 3 — In backtrack(): if path.length === n, push a copy of path to results and return
  4. 4Step 4 — Loop i from 0 to n-1: skip if used[i]; skip if nums[i] === nums[i-1] and !used[i-1] (duplicate pruning)
  5. 5Step 5 — Choose: set used[i] = true, push nums[i] to path
  6. 6Step 6 — Explore: recurse with updated path and used array
  7. 7Step 7 — Unchoose: set used[i] = false, pop nums[i] from path

Combinations and Subsets Pattern

Combinations and subsets are the complement of permutations: order does not matter. The mechanism for enforcing "no order duplicates" is a start index parameter. Instead of choosing from all unused elements at each level, you choose only from elements at index start or later. This ensures that you never add an element that appears earlier in the input than the last element you added, which eliminates ordering duplicates without needing a used array.

Subsets and combinations use the same backtracking skeleton with different collection conditions. For subsets (LeetCode 78), you record path at every single node of the decision tree — including the empty path at the root. For combinations (LeetCode 77 — choose k elements), you record path only when path.length === k (target depth). Combination Sum (LeetCode 39) is a variant where you can reuse elements: the start index on the recursive call stays at i (not i+1), allowing re-selection of the same element.

Handling duplicates in subsets (LeetCode 90) follows the same pattern as Permutations II: sort first, then skip nums[i] if i > start and nums[i] === nums[i-1]. The i > start condition prevents skipping the very first occurrence within a given recursive call (you must try the value at least once), while eliminating all subsequent occurrences of the same value at the same tree depth.

ℹ️

Subsets and Combinations: Same Skeleton, Different Collection Condition

Combinations and subsets use the identical backtracking skeleton. The only difference is when you collect results: subsets collect at every node (including the empty path), while combinations collect only when the path reaches the target length. Master one and you have mastered both — and Combination Sum is just a variant where the start index does not advance after choosing an element.

Constraint Satisfaction Pattern

Constraint satisfaction problems like N-Queens (LeetCode 51) and Sudoku Solver (LeetCode 37) are backtracking problems where the "choices" at each step are heavily restricted by problem rules. For N-Queens, placing a queen in column c at row r is only valid if no previously placed queen shares the same column, diagonal, or anti-diagonal. The backtracking structure is identical — iterate over columns, check if the placement is valid, place, recurse to the next row, remove — but the validity check is the heart of the solution.

The key to efficient constraint satisfaction backtracking is O(1) constraint checking. For N-Queens, maintain three sets: one for occupied columns, one for occupied diagonals (row - col is constant along a diagonal), and one for occupied anti-diagonals (row + col is constant). Before placing a queen, check all three sets in O(1) rather than scanning the board. For Sudoku, maintain sets for each row, column, and 3x3 box — again O(1) per check, O(1) update.

Pruning in constraint satisfaction is more aggressive than in permutations or subsets because the validity check typically eliminates many choices before recursing. N-Queens on an 8x8 board has 8^8 = 16 million possible placements but only 92 valid solutions — the pruning ratio exceeds 99.9%. Writing the constraint check correctly and early (before recursing, not after) is what makes constraint satisfaction backtracking tractable for problem sizes used in coding interviews.

  1. 1Step 1 — Identify the iteration axis: for N-Queens, iterate rows (one queen per row); for Sudoku, iterate empty cells in order
  2. 2Step 2 — Initialize O(1) constraint data structures: sets for columns, diagonals, anti-diagonals (N-Queens); sets per row, column, 3x3 box (Sudoku)
  3. 3Step 3 — In backtrack(row): base case when row === n (N-Queens: record board; Sudoku: return true)
  4. 4Step 4 — Loop over choices (columns for N-Queens, digits 1-9 for Sudoku)
  5. 5Step 5 — Check constraints in O(1): if choice violates any constraint, skip (continue)
  6. 6Step 6 — Choose: update board and constraint sets
  7. 7Step 7 — Explore: recurse to next row/cell; for Sudoku return true immediately if recursion succeeds
  8. 8Step 8 — Unchoose: restore board cell and remove from constraint sets

Backtracking vs Dynamic Programming

Backtracking and dynamic programming are both techniques for solving problems with overlapping subproblems, but they serve fundamentally different goals. Backtracking enumerates all solutions — it is the right tool when you need every valid arrangement, every valid path, or every valid assignment. Dynamic programming optimizes over solutions — it is the right tool when you need the minimum cost, maximum profit, or number of ways to achieve a goal. The question "do I need all solutions or just the best/count?" is the primary decision fork.

Some problems accept both approaches with different performance characteristics. Word Break II (LeetCode 140) asks for all valid word break strings — backtracking with memoization is appropriate. Coin Change (LeetCode 322) asks for the minimum number of coins — DP is strictly better because backtracking would exponentially re-explore identical subproblems. Partition Equal Subset Sum (LeetCode 416) asks whether a valid partition exists — DP is canonical, but backtracking with pruning also passes on LeetCode test cases.

A practical heuristic: if the problem asks "find all X" or "generate all X," reach for backtracking. If it asks "count the number of ways," try DP first — backtracking generates and counts every individual solution, which is exponentially slower than DP's bottom-up counting. If it asks "find the minimum/maximum," DP almost always dominates.

⚠️

Count the Number of Ways? Reach for DP First

If the problem asks "count the number of ways," consider DP before backtracking. Backtracking finds all solutions and counts them — which is correct but exponentially slower than DP, which counts bottom-up without enumerating each solution individually. Backtracking shines when you need to return the actual solutions, not just the count.

Problem Roadmap

The recommended backtracking study path starts with the pure enumeration problems that directly map to the template, then moves to problems with pruning conditions, and finishes with constraint satisfaction problems that require O(1) constraint checking. Working through problems in this order builds the template intuition first and layers complexity incrementally rather than confronting constraint management before the core recursion structure is automatic.

The starter problems are Subsets (78) and Permutations (46) — both can be solved with the template almost verbatim. Subsets II (90) and Permutations II (47) add duplicate handling. Combination Sum (39) introduces reuse of elements. Palindrome Partitioning (131) adds a string validity check as the pruning condition. Word Search (79) brings backtracking into a 2D grid context. N-Queens (51) and Sudoku Solver (37) are the full constraint satisfaction problems.

For interview preparation, being able to solve the medium-tier problems fluently — Combination Sum, Palindrome Partitioning, Word Search — is sufficient for most FAANG coding rounds. N-Queens and Sudoku Solver are genuine hard problems that appear in senior-level interviews. Solidify the medium tier first, then tackle the hards as stretch problems once the template is second nature.

  • Starter: Subsets (78), Permutations (46) — pure template, no pruning
  • Starter: Subsets II (90), Permutations II (47) — add duplicate handling via sort + skip
  • Medium: Combination Sum (39) — reusable elements; Combination Sum II (40) — no reuse, has duplicates
  • Medium: Palindrome Partitioning (131) — string validity as the pruning condition
  • Medium: Word Search (79) — backtracking on a 2D grid with visited marking
  • Hard: N-Queens (51) — constraint sets for columns, diagonals, anti-diagonals
  • Hard: Sudoku Solver (37) — constraint sets per row, column, 3x3 box
  • Hard: Word Break II (140) — backtracking + memoization to prune repeated subproblems

Ready to master algorithm patterns?

YeetCode flashcards help you build pattern recognition through active recall and spaced repetition.

Start practicing now