Backtracking

Mediumbacktracking · recursion · dfs · combinatorics · pruning⏱ 6 min readUpdated

Enumerate solutions by extending a partial candidate one choice at a time, recursing, then undoing the choice, while pruning branches that cannot lead anywhere.

PatternBacktracking

Instinct Return ALL combinations, permutations, partitions or placements - or ask if any arrangement works - build a candidate step by step and undo each choice.

Memory hack Choose, explore, un-choose - walk the decision tree and snip a branch the moment it can no longer succeed.

At a glance

When to use it

  • You must return all combinations, permutations, subsets or partitions
  • You must decide whether any valid arrangement or placement exists
  • Constraints are small, around n <= 20
  • Choices are made step by step and some prefixes can be ruled out early

Key idea

Build one candidate a choice at a time on a shared path, recurse, then undo the choice so siblings start clean, and prune any branch that already breaks a rule. Since the output itself is exponential, clean enumeration with early pruning is the best you can do and beats generating and filtering every raw arrangement.

Typical complexity

Exponential time, e.g. O(n · 2^n) or O(n · n!), O(n) recursion space

How to spot it If the problem says "return all possible", "every arrangement" or "place N items with no conflicts" and asks for the full list or whether one exists, think backtracking first.

The instinct

When the output is "every valid X", the answer itself can be exponential in size, so no clever trick avoids enumeration - the job is to enumerate cleanly and prune early. Picture a decision tree: each level makes one choice (take this number, place a queen in this column, cut the string here). Depth-first search walks the tree, keeps a single mutable path, and restores it after each child so siblings start from the same state.

Subsets, Permutations and Combination Sum are the three skeletons; almost every other backtracking problem, including N-Queens and Word Search, is one of them with extra constraints.

Recognise it

Signal in the problem Why it points here
"return all possible...", "list every..." The output is exponential, so enumeration is the algorithm.
"all subsets / the power set" Each element is either in or out; record a result at every node.
"all permutations / arrangements" Every position may take any unused element: track a used[] array.
"combinations that sum to target" (reuse allowed, or each used once) A start index stops reordered duplicates; pass i or i + 1.
"partition the string so every piece is a palindrome" Choose where the next piece ends, recurse on the remainder.
"place N queens / fill the board with no conflicts" Constraint satisfaction: prune any move that breaks a rule.
"does the word exist in the grid" DFS that marks a cell, explores neighbours, then unmarks it.
Constraints like n <= 10 to 20 Exponential time is acceptable, and is usually expected.

Not this pattern if...

  • Only a count or a best value is asked and subproblems repeat - that is DP (counting coin-change ways, minimum coins).
  • n is around 10^5 - exponential search is impossible; look for greedy, DP or a formula.
  • You need a shortest path in a grid - BFS finds it without exploring every path.
  • You only need reachability in a graph - plain DFS with a permanent visited set, no undo.

Memory hack

Tip

Choose, explore, un-choose. Choose an option and apply it to the shared state. Explore by recursing one level deeper. Un-choose by reversing exactly what you applied, so the next sibling sees an untouched state. Prune before exploring whenever the partial state already breaks a rule.

Analogy - the maze and the ball of string. You explore a maze while unrolling string behind you. At each fork you try one corridor. At a dead end you wind the string back to the last fork and try the next corridor. Pruning is a sign at a corridor entrance saying "dead end ahead" - you don't even walk in.

Say it out loud:

  1. "My state is the path so far; the choices at each level are ___."
  2. "When ___ I record a copy of the path (and stop, or keep going for subsets)."
  3. "I prune when ___, skip duplicates by sorting and skipping equal siblings, and undo every change after the recursive call."

How it works

Decision tree for Subsets of [1, 2, 3] using a start index. Every node is a valid subset, so all 8 get recorded. Because each level only offers elements after the last one chosen, [2, 1] can never appear as a duplicate of [1, 2].

flowchart TD
  R["[ ]"] --> A["[1]"]
  R --> B["[2]"]
  R --> C["[3]"]
  A --> D["[1, 2]"]
  A --> E["[1, 3]"]
  D --> F["[1, 2, 3]"]
  B --> G["[2, 3]"]

The other families change only what a level may choose: permutations scan every index not yet used, partitioning chooses the end of the next piece, and grid search chooses one of four neighbours.

Java template

java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

class BacktrackingTemplate {
    // Combinations family (Combination Sum II shape). Sort so duplicates sit together and pruning works.
    static List<List<Integer>> combinations(int[] nums, int target) {
        Arrays.sort(nums);
        List<List<Integer>> out = new ArrayList<>();
        dfs(nums, 0, target, new ArrayList<>(), out);
        return out;
    }

    static void dfs(int[] nums, int start, int remain, List<Integer> path, List<List<Integer>> out) {
        if (remain == 0) { out.add(new ArrayList<>(path)); return; }  // goal: store a COPY
        for (int i = start; i < nums.length; i++) {
            if (i > start && nums[i] == nums[i - 1]) continue;         // skip duplicate siblings
            if (nums[i] > remain) break;                                // prune (positive, sorted input)
            path.add(nums[i]);                                          // choose
            dfs(nums, i + 1, remain - nums[i], path, out);              // explore; pass i if reuse is allowed
            path.remove(path.size() - 1);                               // un-choose
        }
    }

    // Permutations family: every position may take any element not yet used.
    static void permute(int[] nums, boolean[] used, List<Integer> path, List<List<Integer>> out) {
        if (path.size() == nums.length) { out.add(new ArrayList<>(path)); return; }
        for (int i = 0; i < nums.length; i++) {
            if (used[i]) continue;
            used[i] = true;  path.add(nums[i]);                         // choose
            permute(nums, used, path, out);                             // explore
            used[i] = false; path.remove(path.size() - 1);              // un-choose
        }
    }
}

For Subsets, drop the target check and add a copy of path at the top of every call. For grid problems like Word Search, "choose" means overwriting the cell with a marker and "un-choose" means restoring the original letter.

Complexity

Problem shape Time (output-sensitive) Extra space
Subsets O(n · 2^n) O(n) recursion + output
Permutations O(n · n!) O(n)
Combination sum Exponential: roughly branches^(target / smallest) O(target / smallest) depth
N-Queens Bounded by O(n!) and far less with pruning O(n)
Word Search O(cells · 3^L) for word length L O(L)

Pruning rarely changes the worst-case bound, but in practice it is the difference between passing and timing out.

Variations

  • Subsets - record at every node. Subsets.
  • Combinations with a target - reuse allowed means recurse with i (Combination Sum); use-once with duplicate input means i + 1 plus sibling skipping (Combination Sum II).
  • Permutations - used[] array; with duplicates, sort and skip nums[i] == nums[i-1] && !used[i-1]. Permutations.
  • Cartesian product - each position chooses from its own list. Letter Combinations of a Phone Number.
  • Generation with counters - add ( while open < n, add ) while close < open. Generate Parentheses.
  • Partitioning - choose the next cut; precompute a palindrome table. Palindrome Partitioning.
  • Constraint placement - sets for columns and both diagonals (r - c, r + c). N-Queens.
  • Grid path search - mark, explore four directions, unmark. Word Search.

Common mistakes

  • Forgetting to undo one piece of state - the used flag, the grid marker, or a StringBuilder's length.
  • Using i > 0 instead of i > start for duplicate skipping, which wrongly drops valid answers such as [1, 1, 6].
  • Recursing with i + 1 when reuse is allowed (missing answers), or with i when it is not (infinite reuse).
  • Pruning with break on unsorted input or with negative numbers - later candidates might still fit.
  • Building strings with + at every level; a shared StringBuilder with setLength is cheaper.
Warning

out.add(path) stores a reference to the one list you keep mutating, so after the search every result points to the same (usually empty) list. Always store a snapshot: out.add(new ArrayList<>(path)).

All practice problems

9 problems on this site use this pattern, easiest first. Solve them in order and say the instinct out loud before you code.

My notes saved in this browser