Skip to main content

Backtracking

Overviewโ€‹

Backtracking searches a space of candidate solutions by building them one choice at a time, and abandoning a partial candidate the moment it cannot possibly lead to a valid one. It is depth-first search over an implicit tree of choices, with pruning.

The pruning is the entire point. Without it this is brute force; with a good constraint check it can reduce a space of 10ยฒโฐ candidates to a few thousand actually explored.

Core Conceptsโ€‹

TermMeaning
ChoiceA decision at the current step โ€” which value, which position, which branch
ConstraintA rule the partial solution must satisfy
GoalThe condition marking a complete solution
PruningAbandoning a branch once it cannot satisfy the constraints
UndoRestoring state when returning from a branch โ€” the "backtrack"

Architecture / Mechanismโ€‹

Every backtracking algorithm has the same shape:

def backtrack(state, choices):
if is_goal(state):
record(state)
return
for choice in choices:
if not is_valid(state, choice):
continue # prune: this branch cannot work
apply(state, choice) # make the choice
backtrack(state, next_choices) # recurse
undo(state, choice) # UNDO โ€” the defining step

The undo is what distinguishes backtracking from ordinary recursion. Because the state is shared and mutated in place, each branch must leave it exactly as it found it.

N-queensโ€‹

Place N queens on an Nร—N board so none attack another.

def solve_n_queens(n):
solutions = []
cols, diag, anti = set(), set(), set()
placement = []

def place(row):
if row == n:
solutions.append(list(placement))
return
for col in range(n):
# Two queens share a diagonal iff row-col matches; an anti-diagonal iff row+col does
if col in cols or (row - col) in diag or (row + col) in anti:
continue # prune
cols.add(col); diag.add(row - col); anti.add(row + col)
placement.append(col)

place(row + 1)

placement.pop() # undo
cols.remove(col); diag.remove(row - col); anti.remove(row + col)

place(0)
return solutions

The three sets are what make this fast. Checking conflicts in O(1) rather than rescanning the board turns an impractical search into one that solves n = 8 instantly. The quality of the pruning check determines whether backtracking is usable at all.

Placing one queen per row is itself a form of pruning โ€” it removes every arrangement with two queens in a row from consideration without ever generating one, cutting the space from C(64, 8) โ‰ˆ 4.4 billion to 8โธ โ‰ˆ 16.7 million before any constraint check runs.

Permutations and subsetsโ€‹

The two most common shapes, worth recognising:

def permutations(items):
result, current, used = [], [], [False] * len(items)

def build():
if len(current) == len(items):
result.append(list(current)) # copy โ€” `current` keeps mutating
return
for i, x in enumerate(items):
if used[i]:
continue
used[i] = True; current.append(x)
build()
current.pop(); used[i] = False # undo
build()
return result

def subsets(items):
result, current = [], []

def build(i):
if i == len(items):
result.append(list(current))
return
build(i + 1) # exclude items[i]
current.append(items[i])
build(i + 1) # include items[i]
current.pop() # undo
build(0)
return result

Permutations are O(n!) and subsets O(2โฟ) โ€” both unavoidable, since that is how many outputs there are. Backtracking does not make these problems cheap; it makes constrained versions cheap, where pruning removes most branches.

Practical Usageโ€‹

ProblemChoice per stepPruning rule
N-queensColumn for this rowNo shared column or diagonal
SudokuDigit for this cellNot already in the row, column or box
Maze solvingDirection to moveNot a wall, not already visited
Word search in a gridAdjacent cellMatches the next character
Subset sumInclude or excludeRunning sum โ‰ค target
Graph colouringColour for this vertexDiffers from every coloured neighbour
Regular-expression matchingConsume or skipPattern still able to match
Constraint solvers, SATVariable assignmentNo clause falsified
Order your choices to prune early

Trying the most constrained option first prunes far more of the tree. In Sudoku, filling the cell with the fewest legal digits (rather than the next cell in reading order) is the difference between milliseconds and minutes.

This is the most-constrained-variable heuristic, and it is the single highest-value improvement to almost any backtracking search.

Edge Cases & Pitfallsโ€‹

Forgetting to undo corrupts every later branch

The undo step must reverse everything the branch changed. A missed pop(), an unreleased set entry, or a mutated field leaks into sibling branches, and the result is missing or duplicated solutions rather than a crash.

Two defences: keep the mutation and its undo adjacent in the source so the pairing is visible, or pass immutable state down instead of mutating shared state โ€” simpler and much harder to get wrong, at the cost of copying.

  • Appending the working state instead of a copy. result.append(current) stores a reference that keeps mutating; every entry ends up identical (usually empty). Always list(current).
  • No pruning means brute force. If is_valid always returns true, you are enumerating the whole space. Check that the constraint actually eliminates branches.
  • Recursion depth. Depth equals solution length; deep searches need an explicit stack.
  • Exponential worst case is inherent. Backtracking finds optimal answers to NP-hard problems, but no pruning makes the worst case polynomial. Beyond a certain size you need approximation, dynamic programming if subproblems overlap, or a dedicated solver.
  • Finding one solution vs. all. Return early for one; the difference is often orders of magnitude.

Comparisonsโ€‹

BacktrackingDPGreedy
ExploresAll branches, prunedAll subproblems, memoisedOne path
MemoryO(depth)O(states)O(1)
ComplexityExponential, prunedPolynomialO(n log n)
Use whenThe state space is too large to tabulateSubproblems overlapThe greedy choice is provably safe
ReturnsAll solutions, or the bestThe optimal valueOne answer

Referencesโ€‹

  • Cormen, Leiserson, Rivest & Stein, Introduction to Algorithms, Ch. 34โ€“35 โ€” NP-completeness and approximation, the context in which backtracking is usually the practical answer.
  • Knuth, D., The Art of Computer Programming, Vol. 4B, ยง7.2.2 โ€” backtracking in depth, including dancing links for exact-cover problems.

Books & Videosโ€‹

  • Knuth, D., "Dancing Links" โ€” Algorithm X for exact cover, and the fastest known Sudoku solver.