Backtracking is a depth-first search over a tree of partial solutions. Almost every backtracking problem is the same skeleton with four pluggable pieces; recognizing them lets you solve unseen problems on sight.
The canonical template
List<List<Integer>> results = new ArrayList<>();
void backtrack(List<Integer> path, /* problem state */ int start) {
if (isComplete(path)) { // 1. base / goal test
results.add(new ArrayList<>(path)); // snapshot — copy, don't alias!
return;
}
for (int choice = start; choice < options; choice++) {
if (!isValid(choice, path)) continue; // 4. pruning
path.add(choice); // 2a. choose
backtrack(path, choice + 1); // 2b. explore
path.remove(path.size() - 1); // 3. unchoose (undo)
}
}
The four building blocks
- Goal / base case — when is a partial solution complete? Record it (always a copy —
new ArrayList<>(path)— becausepathkeeps mutating after you return). - Choices — the loop over options at this node. A
startindex avoids reusing earlier elements (combinations/subsets); iterating all elements with aused[]flag allows reordering (permutations). - Choose / unchoose — mutate the shared
pathgoing down, undo it coming back up. One buffer serves the entire tree. - Pruning — skip choices that violate a constraint or cannot beat the best answer. This is where the asymptotics actually live.
Why one shared buffer
Allocating a fresh list per node would be O(n) per call and bury you in garbage. The choose/unchoose pair keeps a single path that is the current root-to-node walk; the undo restores invariant state for the sibling branches.