Blog/PATTERNS

Backtracking: prune before you generate

The difference between a backtracking solution that finishes and one that does not is almost never the language. It is whether you cut branches before walking them.

KT
Kenji TanakaProblem setting
25 June 2026·7 min read

Backtracking is exhaustive search with an undo. Written naively it explores everything and times out; written with pruning it explores a small fraction of the same tree and finishes instantly. The algorithm is identical — only the order and the early exits change.

The skeleton

python
1def backtrack(state, choices):
2 if is_complete(state):
3 record(state)
4 return
5 for choice in choices:
6 if not is_valid(state, choice):
7 continue # prune
8 apply(state, choice)
9 backtrack(state, next_choices(choices, choice))
10 undo(state, choice) # the backtrack

The undo is what makes it backtracking rather than plain recursion, and it is where the bugs live. Every mutation on the way down needs an exact inverse on the way up — including the ones you forgot were mutations.

Generate-and-filter versus prune

The naive N-Queens generates every arrangement and checks validity at the leaves: n! leaves for n queens. Checking validity as you place each queen cuts entire subtrees at the moment they become impossible. For n = 8 that is the difference between 40,320 complete arrangements examined and about 2,000 partial states visited.

python
1def solve(n):
2 cols, diag, anti = set(), set(), set()
3 board, out = [], []
4
5 def place(r):
6 if r == n:
7 out.append(board[:])
8 return
9 for c in range(n):
10 if c in cols or (r - c) in diag or (r + c) in anti:
11 continue # prune here, not at the leaf
12 cols.add(c); diag.add(r - c); anti.add(r + c); board.append(c)
13 place(r + 1)
14 board.pop(); anti.discard(r + c); diag.discard(r - c); cols.discard(c)
15
16 place(0)
17 return out
The three sets

A queen at (r, c) occupies column c, diagonal r − c and anti-diagonal r + c. Those two subtractions turn an O(n) conflict scan into O(1), and they are worth memorising outright.

Handling duplicates

Generating subsets or permutations of an input with repeated values produces duplicate outputs unless you suppress them. Sort first, then skip a choice when it equals the previous one and the previous one was not used at this level.

python
1nums.sort()
2for i, x in enumerate(nums):
3 if used[i]:
4 continue
5 if i > 0 and nums[i] == nums[i-1] and not used[i-1]:
6 continue # this branch was already explored
7 ...

Deduplicating the output afterwards with a set gives the same answers and does the same wasted work. The point of the skip is that the duplicate branch is never walked.

Ordering choices matters

For constraint problems, trying the most constrained option first tends to cut the tree fastest — this is the standard heuristic in Sudoku solvers, where filling the cell with fewest candidates first turns an intractable search into an instant one. The choices are the same; the order changes how quickly contradictions surface.

When to stop optimising

Backtracking is exponential and pruning changes the base, not the exponent. If n is 20 and the search is 2^n, pruning may well be enough. If n is 200, no pruning will save it and the problem wants dynamic programming or a greedy argument instead. Read the constraint line before deciding backtracking is the answer.

backtrackingrecursionsearch
Practise this

Reading about a pattern is not the same as producing it under time pressure. The problems that drill this are in the curriculum, in order.

Related reading.