Pattern

Backtracking

Explore all possibilities by building solutions incrementally and abandoning paths that can't lead to valid solutions.

Roadmap

  1. What Is This Pattern?
  2. When to Use It
  3. The Template
  4. Use Case 1 — Subsets
  5. Use Case 2 — Permutations
  6. Use Case 3 — Combination Sum
  7. Pruning Strategies
  8. Common Problems
  9. Interactive Demo
  10. Complexity Analysis
  11. Key Takeaways
Step 01

What Is This Pattern?

Backtracking is DFS + pruning. You build a solution one step at a time. At each step you try all available options. If the current path cannot lead to a valid solution, you backtrack — undo the last choice and try the next option.

The Core Idea: A Decision Tree

Every backtracking problem can be visualized as a tree of choices. Each node represents a partial solution. Branches represent adding a new choice. Leaf nodes are complete solutions (or dead ends that get pruned).

[ ] [1] pick 1 [4] pick 4 [1,2] [1,3] [1,2,3] [1,2,4] [1,3,4] ... explored path pruned branch valid solution × pruned (violates constraint)
💡

Think of it as a maze. You walk forward, making choices at each fork. When you hit a dead end, you walk backward (backtrack) to the last fork and try a different direction. Without pruning, this is brute-force DFS. With pruning, you skip entire hallways you know lead nowhere.

Step 02

When to Use It

Look for these recognition signals in the problem statement. If you spot one, backtracking is very likely the intended approach.

"Generate all" / "Find all possible"
Combinations, permutations, subsets, or any exhaustive enumeration.
Constraint satisfaction
Sudoku solver, N-Queens, crossword filling — place values subject to rules.
Path finding with constraints
Find a path in a grid/graph that visits specific cells or satisfies a property.
Exhaustive enumeration
When the problem explicitly asks for all valid configurations, not just the count or the optimal one.

Backtracking vs. DP: Both explore subproblems recursively. Use DP when subproblems overlap and you only need an optimal value. Use backtracking when you need to enumerate all solutions or when the state space has no useful overlapping subproblems.

Step 03

The Template

Nearly every backtracking problem follows this universal three-phase template: choose, explore, unchoose.

Universal Backtracking Template

void backtrack(List<Integer> path, int[] choices, int start) {
    if (isComplete(path)) {
        result.add(new ArrayList<>(path));  // save a copy
        return;
    }

    for (int i = start; i < choices.length; i++) {
        if (!isValid(choices[i])) continue;  // prune

        path.add(choices[i]);           // 1. CHOOSE
        backtrack(path, choices, i + 1); // 2. EXPLORE
        path.remove(path.size() - 1);  // 3. UNCHOOSE
    }
}
1. Choose

Add the current option to your partial solution. This moves you deeper into the decision tree.

2. Explore

Recursively explore all possibilities that build on this choice. This is the DFS step.

3. Unchoose

Remove the choice to restore state. This is the backtrack — moving back up the tree to try the next branch.

📝

Why copy the path? In result.add(new ArrayList<>(path)) we create a snapshot because path is mutated in place. Without the copy, every entry in result would point to the same (eventually empty) list.

Step 04

Use Case 1 — Subsets

Generate all subsets (the power set) of [1, 2, 3]. At each element, you have two choices: include it or skip it. Every node in the tree is a valid subset.

Decision Tree for Subsets of [1, 2, 3]

At each level, we decide whether to include the next element. Every path from root to any node is a valid subset.

[ ] +1 skip 1 [1] +2 skip [ ] +2 skip [1,2] [1] [2] [ ] [1,2,3] [1,2] [1,3] [1] [2,3] [2] [3] [ ] +3 sk +3 sk +3 sk +3 sk

Every leaf is a subset. Total: 23 = 8 subsets. In the code, we collect the path at every recursive call (not just at the leaves).

Code: Subsets

List<List<Integer>> result = new ArrayList<>();

void subsets(int[] nums) {
    backtrack(nums, 0, new ArrayList<>());
}

void backtrack(int[] nums, int start, List<Integer> path) {
    result.add(new ArrayList<>(path));   // every path is a valid subset

    for (int i = start; i < nums.length; i++) {
        path.add(nums[i]);                // choose
        backtrack(nums, i + 1, path);     // explore
        path.remove(path.size() - 1);    // unchoose
    }
}
💡

Key difference from permutations: Subsets use a start index to avoid duplicates. Element [1,2] and [2,1] are the same subset, so we only ever pick elements that come after the current index.

Step 05

Use Case 2 — Permutations

Generate all permutations of [1, 2, 3]. Unlike subsets, order matters: [1,2,3] and [3,2,1] are different. At each level we can pick any unused element.

Decision Tree for Permutations of [1, 2, 3]

[ ] pick 1 pick 2 pick 3 [1] [2] [3] [1,2] [1,3] [2,1] [2,3] [3,1] [3,2] [1,2,3] [1,3,2] [2,1,3] [2,3,1] [3,1,2] [3,2,1]

3 choices at level 1 × 2 at level 2 × 1 at level 3 = 3! = 6 permutations.

Code: Permutations

void backtrack(int[] nums, List<Integer> path, boolean[] used) {
    if (path.size() == nums.length) {
        result.add(new ArrayList<>(path));
        return;
    }

    for (int i = 0; i < nums.length; i++) {
        if (used[i]) continue;             // skip already-used

        used[i] = true;                   // choose
        path.add(nums[i]);
        backtrack(nums, path, used);       // explore
        path.remove(path.size() - 1);    // unchoose
        used[i] = false;
    }
}
🎯

No start index here. Unlike subsets, permutations always loop from index 0 because order matters. Instead, we use a used[] boolean array to track which elements are already in the current path.

Step 06

Use Case 3 — Combination Sum

Find all combinations from [2, 3, 6, 7] that sum to target = 7. Each number can be reused unlimited times. This is where pruning becomes essential.

Decision Tree with Pruning (target = 7)

When the running sum exceeds the target, we prune the entire subtree. Sorting the array lets us prune even earlier: once sum + candidates[i] > target, all subsequent candidates are also too large.

sum=0 [ ] +2 +3 +6 +7 sum=6 [6] sum=7 [7] sum=2 [2] +2 +3 +6 sum=4 [2,2] +2 +3 s=6 [2,2,2] prune s=7 [2,2,3] sum=5 [2,3] prune s=8 sum=3 [3] sum=6 [3,3] prune s=9 Results: [2,2,3] and [7] 6 branches pruned

Code: Combination Sum

void backtrack(int[] cands, int target, int start, List<Integer> path) {
    if (target == 0) {
        result.add(new ArrayList<>(path));
        return;
    }

    for (int i = start; i < cands.length; i++) {
        if (cands[i] > target) break;    // prune: sorted, so all later > target too

        path.add(cands[i]);               // choose
        backtrack(cands, target - cands[i],
                  i, path);               // explore (i, not i+1: reuse allowed)
        path.remove(path.size() - 1);   // unchoose
    }
}

Notice the break, not continue. Because the array is sorted, once cands[i] > target, every element after it is also too large. We can skip the entire rest of the loop. This is the power of sorting + pruning.

Step 07

Pruning Strategies

Pruning is what separates practical backtracking from impractical brute force. The more branches you can eliminate early, the faster your algorithm runs.

Without Pruning

Explore every branch to its leaf, even when the sum already exceeds the target.

63 nodes
visited for [1..6], target=6

With Pruning

Sort first, then break as soon as current candidate exceeds remaining target.

18 nodes
71% of branches eliminated

The Four Key Pruning Techniques

1. Sort First for Early Termination
Sort the input array. When candidates[i] > remaining, use break instead of continue to skip all larger elements at once.
2. Skip Duplicates
When the input has duplicate values, skip them to avoid generating duplicate results:
if (i > start && nums[i] == nums[i-1]) continue;
3. Track Used Elements
For permutation-type problems, a boolean[] used array prevents picking the same element twice. This eliminates invalid branches immediately.
4. Constraint Propagation
In constraint problems (Sudoku, N-Queens), check constraints before placing a value. If no valid value exists, prune immediately instead of recursing deeper.
💡

Rule of thumb: Always ask "can I reject this branch early?" before recursing. Sorting costs O(n log n) but can turn an exponential search into something much more tractable by pruning entire subtrees.

Step 08

Common Problems

These classic LeetCode problems use backtracking. They are listed in rough order of difficulty within each category.

#78 Subsets Medium
#46 Permutations Medium
#39 Combination Sum Medium
#17 Letter Combinations of a Phone Number Medium
#22 Generate Parentheses Medium
#79 Word Search Medium
#51 N-Queens Hard
💡

Practice tip: Start with #78 (Subsets) and #46 (Permutations) to internalize the template. Then tackle #39 (Combination Sum) to learn pruning. Once comfortable, #51 (N-Queens) is the ultimate constraint-satisfaction backtracking problem.

Step 09

Interactive Demo

Enter an array of numbers and step through the subset generation. Watch the backtracking algorithm build the decision tree, collect results, and backtrack when needed.

⚙ Subset Generator with Backtracking Visualization


Current Path:
[ ]
Press "Step →" or "Run All" to begin.
Results Collected:
No results yet.
Step 10

Complexity Analysis

Backtracking complexity varies dramatically depending on the problem type. Here are the three fundamental cases:

Subsets
O(2n)
n elements, include/exclude each
Permutations
O(n!)
n choices, then n-1, then n-2...
Combinations
O(C(n,k))
n choose k, depends on target

Why Exponential?

Subsets: Each of the n elements has 2 choices (in or out), giving 2n total subsets. Even the optimal algorithm must at least enumerate all of them. Time: O(n * 2n) because each subset takes O(n) to copy.

Permutations: n choices for position 1, n-1 for position 2, etc. Total: O(n * n!) including the copy step.

Space: O(n) for the recursion stack depth (the path and the call stack), plus O(answer_size) for storing all results.

Pruning does not change the worst case but it often dramatically reduces the average case. In practice, a well-pruned backtracking solution runs orders of magnitude faster than the theoretical worst case.

Step 11

Key Takeaways

01

Choose → Explore → Unchoose. This three-phase template is the skeleton of every backtracking solution. Memorize it. The only things that change between problems are what "choose" means and what "complete" means.

02

Pruning is the differentiator. Without pruning, backtracking is just brute-force DFS. Sort the input, skip duplicates, and check constraints early to eliminate entire subtrees before wasting time exploring them.

03

Three fundamental types. Subsets (use start index, collect at every node), Permutations (use used[] array, collect at leaves only), and Combinations (like subsets but with a target constraint). Every backtracking problem is a variation of one of these three.

04

Visualize the decision tree. Before writing code, draw the tree of choices. This makes it obvious what your loop variable is, when to prune, and what the base case should be. The code writes itself once you have the tree.

05

Do not forget to copy and restore. Two classic bugs: forgetting to copy the path before adding it to results (all results end up as the same empty list), and forgetting to undo the choice (the path grows without bound). The choose/unchoose pair must always be symmetric.