Backtracking Basics
Backtracking is a way to find every solution, or one solution, by building it one choice at a time. At each step you make a choice, continue building, and when you reach the end or a dead end, you undo the last choice and try the next option.
It is how you would solve a maze by hand: walk forward, and at a dead end, go back to the last junction and take a different turn. This page explains the idea step by step, gives a template that fits almost every backtracking problem, and shows how to make it fast.
Step 1: See the Problem as a Tree of Choices
Every backtracking problem is a sequence of decisions. Draw them as a tree:
- the root is the empty starting state, with nothing chosen yet;
- each branch is one choice;
- each path from the root is a partial solution;
- a leaf is either a complete solution or a dead end.
For "all subsets of [1, 2, 3]", each level decides which number to add next, and only numbers after the last one chosen are allowed, so no subset appears twice:
| Depth | Partial solutions at this depth |
|---|---|
| 0 (root) | [] |
| 1 | [1][2][3] |
| 2 | [1, 2][1, 3][2, 3] |
| 3 | [1, 2, 3] |
The algorithm visits this tree depth-first: it goes all the way down one branch before trying the next. Here every node is a valid subset, so all 8 are recorded.
Step 2: Choose, Explore, Un-choose
Walking the tree uses one recursive function with three moves inside a loop over the options:
- Choose: add an option to the current partial solution.
- Explore: call the function again to build the rest.
- Un-choose: remove that option again, so the next option starts from the same state.
The un-choose step is what gives backtracking its name. Without it, the choices from one branch would leak into the next one.
import java.util.ArrayList;
import java.util.List;
public class Template {
// All subsets of nums: the smallest complete backtracking program
public static List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
backtrack(nums, 0, new ArrayList<>(), result);
return result;
}
private static void backtrack(int[] nums, int start, List<Integer> current,
List<List<Integer>> result) {
result.add(new ArrayList<>(current)); // record the current state (a copy)
for (int i = start; i < nums.length; i++) {
current.add(nums[i]); // 1. choose
backtrack(nums, i + 1, current, result); // 2. explore
current.remove(current.size() - 1); // 3. un-choose
}
}
public static void main(String[] args) {
System.out.println(subsets(new int[] {1, 2, 3}));
// [[], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3], [3]]
}
}
Two details trip people up:
- Save a copy.
, notresult.add(new ArrayList<>(current)) . There is only oneresult.add(current) list; without the copy, every saved result would be the same list, and it ends up empty.current - Undo exactly what you did. If you added to a list, remove from it; if you marked a cell as used, unmark it.
Step 3: Answer Four Questions for Each Problem
To adapt the template to a new problem, decide:
| Question | Subsets | Permutations | N-Queens |
|---|---|---|---|
| What is the state? | numbers chosen so far | numbers placed so far | queens placed in rows 0 to r - 1 |
| What are the choices? | any number after the last one | any unused number | any column in row r |
| When is a choice invalid? | never | the number is already used | the square is attacked |
| When is it complete? | every state counts | all numbers placed | all n rows filled |
Step 4: Prune Early
Backtracking explores a tree that can be huge, so the main way to make it fast is to stop going down a branch as soon as it cannot lead to a solution. This is called pruning.
- Check validity before recursing. N-Queens checks that a square is safe before placing a queen, instead of placing all queens and checking at the end.
- Sort and stop early. In Combination Sum, once a sorted candidate is larger than the remaining target, every later candidate is too, so the loop can
.break - Bound the answer. In Fair Distribution of Cookies, a branch whose largest share is already worse than the best answer found so far is abandoned.
- Count what is left. In Restore IP Addresses, if the remaining digits cannot fill the remaining parts (each needs 1 to 3 digits), stop.
Good pruning does not change the worst-case complexity, but in practice it often cuts the work by orders of magnitude.
Step 5: Handle Duplicate Input Values
When the input contains equal values, such as [1, 2, 2]
- Sort the input, so equal values sit next to each other.
- At each level of the tree, skip a value if it equals the previous value tried at that same level:
if (i > start && nums[i] == nums[i - 1]) continue;
This allows a 2 to be followed by another 2 deeper in the tree (giving [2, 2]
How Expensive Is It?
Backtracking is usually exponential, because the number of solutions itself is exponential. A useful estimate is number of nodes in the tree × work per node:
| Problem type | Number of results | Typical time |
|---|---|---|
| Subsets of n items | 2n | O(n · 2n), copying each subset |
| Permutations of n items | n! | O(n · n!) |
| Combinations of k from n | C(n, k) | O(k · C(n, k)) |
That is why these problems have small limits, such as n <= 10n <= 20
Problems by Pattern
| Pattern | Problems |
|---|---|
| Subsets: pick items, order does not matter | |
| Combinations with a target sum | |
| Permutations: order matters | |
| One choice per position in a string | |
| Cutting a string into valid pieces | |
| Placing items under constraints | |
| Paths in a grid or graph |