Backtracking and State-Space Search
Imagine you must place eight queens on a chessboard so that no two attack each other. There are 4,426,165,368 ways to place 8 pieces on 64 squares. Only 92 of those placements work. You cannot enumerate billions of boards by hand. Yet a computer exploring them one at a time solves this in milliseconds. It does so by abandoning bad choices the instant they go wrong. This is the essence of backtracking. Build a solution piece by piece. The moment a partial piece cannot possibly lead anywhere valid, stop extending it. Then try something else. Backtracking is the algorithmic backbone of constraint satisfaction. Sudoku solvers, puzzle generators, compiler register allocators, SAT solvers, and AI game engines all lean on some variant of it. The core insight is that most combinatorial search spaces are trees. Every partial solution branches into a small number of extensions. A depth-first traversal of that tree can rule out entire subtrees. This works when combined with cheap local checks, without ever visiting those subtrees. This pruning is what separates a tractable backtracking search from a hopeless brute-force enumeration. This holds true even though both share the same worst-case exponential ceiling. This article treats backtracking as systematic depth-first search (DFS) over a state space of partial solutions. Each state is extended one decision at a time. Infeasible states are abandoned ("backtracked from") immediately. This generalizes ideas you have already seen in recursive formulations of the The 0/1 Knapsack Problem. Deciding "include or exclude" one item at a time is itself a two-way backtracking tree. It is just usually explored with memoization instead of pruning. By the end, you will have a reusable template for backtracking algorithms. You will also gain a rigorous accounting of why the technique is correct. It visits every candidate exactly once, by construction. You will understand why its cost is bounded by the size of the pruned search tree. This holds rather than being bounded by the full solution space. You will also see how this naturally sets up Branch and Bound. That technique adds numerical bounds to prune even more aggressively. It does so for optimization (not just satisfaction) problems.
- Backtracking performs a depth-first search (DFS) over the tree of partial solutions, extending one decision at a time.
- A pruning test ("is this partial solution still promising?") is applied at every state, cutting off entire subtrees before they are built.
- Correctness follows from exhaustiveness: every complete candidate solution corresponds to exactly one root-to-leaf path, so nothing valid is missed.
- Worst-case time is exponential — bounded by the size of the tree actually explored, not the full combinatorial universe.
- The technique unifies constraint satisfaction (N-Queens, Sudoku, graph coloring) and combinatorial enumeration (subsets, permutations, partitions).
- It generalizes directly into Branch and Bound, which adds numeric bounds for optimization problems.
Backtracking is depth-first search over partial solutions that abandons a branch the instant a cheap local test proves it cannot be completed correctly.
The idea
A state space for a combinatorial problem is the (implicit) tree in which the root is the empty solution, each internal node is a partial candidate — a sequence of decisions made so far — and each edge extends the candidate by one more decision. A complete candidate is a leaf: a fully specified assignment (e.g., all 8 queens placed, all cells of a Sudoku filled). Backtracking is simply a depth-first traversal of this tree, with one crucial refinement: at every node, before recursing into a child, we ask a cheap question — can this partial candidate possibly be extended into a valid complete solution? If the answer is provably no, we do not descend into that subtree at all. This is called pruning, and the function that performs the check is often called a constraint check, feasibility test, or bounding function (though "bounding" more properly belongs to Branch and Bound). The key conceptual move that distinguishes backtracking from naive brute force is this: brute force generates every complete candidate and checks each one at the leaf; backtracking checks partial candidates at every internal node and refuses to generate children of an infeasible node. Since infeasibility is often detectable long before a candidate is complete (placing a queen that already attacks an existing queen, for instance), huge subtrees are eliminated with a single constant-time test at their root, rather than being generated and rejected one leaf at a time. Formally, backtracking solves problems expressible as: find one (or all, or the best) sequence such that each is chosen from a finite domain , subject to constraints that can be checked incrementally as each is fixed. This is exactly the structure of Constraint Satisfaction Problems. The recursion always has the same shape: choose a value for the next variable, recurse, and if the recursion fails to complete a solution, undo the choice ("backtrack") and try the next candidate value. The "undo" step is what gives the technique its name — it is what allows the same mutable data structure (a partial board, a partial permutation) to be reused across the entire search without allocating a fresh copy at every node.
How it works
The general backtracking template operates on a partial solution vector and a position index indicating how many decisions have been fixed so far. At each call, it tries every candidate value for the next position, checks feasibility, and recurses only on feasible extensions. ``` function BACKTRACK(partial, k): if k == n: // base case: partial is a complete candidate if partial is a valid complete solution: RECORD(partial) // or: return partial (find-one variant) return // (find-one: return true/false and propagate) for each candidate value v in DOMAIN(k): if IS_CONSISTENT(partial, k, v): // cheap local feasibility test partial[k] = v // make the choice BACKTRACK(partial, k + 1) // explore UNDO(partial, k) // unmake the choice (backtrack) return ``` Three components must be supplied for a specific problem: 1. DOMAIN(k) — the set of candidate values for position (e.g., columns 1..8 for the queen in row ). 2. IS_CONSISTENT(partial, k, v) — a local test: does placing at position violate any constraint with the choices already made in positions ? This must be cheap (ideally or ), since it is invoked at every node. 3. UNDO(partial, k) — restores any auxiliary state (e.g., "column is occupied," "diagonal is occupied") that `IS_CONSISTENT` mutated, so that the next candidate at position is tested against a clean state. The template above finds all solutions. Two common variants adjust the base case and return type: a find-one variant returns `true` as soon as one complete valid candidate is found, propagating that `true` immediately up the call stack without exploring siblings; a count-only variant increments a counter at the base case and never stores solutions, useful when only the number of solutions matters. An optimization variant (the seed of Branch and Bound) instead tracks the best complete solution found and additionally prunes any partial solution whose best possible completion is already worse than the current best — this needs an extra bounding function beyond mere feasibility. Concretely, for the N-Queens problem, `DOMAIN(k)` is columns through for the queen placed in row ; `IS_CONSISTENT` checks that the new column is not already used, and that the new queen shares no diagonal with any previously placed queen (this can be checked in amortized using three boolean arrays indexed by column, and by the two diagonal identities and ). ``` function SOLVE_N_QUEENS(N): cols = array of size N, all False // column occupied? diag1 = array indexed by (row - col), all False // "/" diagonals diag2 = array indexed by (row + col), all False // "\" diagonals placement = array of size N // placement[row] = col count = 0 function PLACE(row): nonlocal count if row == N: count += 1 return for col in 0 .. N-1: d1 = row - col + (N - 1) // shift to keep index nonnegative d2 = row + col if not cols[col] and not diag1[d1] and not diag2[d2]: cols[col] = diag1[d1] = diag2[d2] = True placement[row] = col PLACE(row + 1) cols[col] = diag1[d1] = diag2[d2] = False // undo return PLACE(0) return count ``` Every constraint check here is , and the array-based undo is as well — this is what makes the search efficient in practice, even though the underlying tree is combinatorially large.
Complexity & correctness
Correctness. Backtracking's correctness rests on a simple exhaustiveness argument. Define the candidate tree : the root is the empty partial solution, and node at depth has one child for every value that extends . Every complete candidate solution corresponds to exactly one root-to-leaf path in (the sequence of choices ), and every root-to-leaf path corresponds to exactly one complete candidate. The algorithm as written performs a full DFS of except that it never recurses into a child for which `IS_CONSISTENT` returns false. Correctness of pruning therefore requires exactly one property, which must be proved for each concrete `IS_CONSISTENT`: monotonicity of infeasibility — if a partial solution violates a constraint, then every completion of it also violates that constraint. For N-Queens, if two placed queens already attack each other, no matter how you place the remaining queens the board is invalid; the property holds because attacks between already-placed queens are never "undone" by future placements. Given this property, skipping an infeasible subtree loses no valid complete solutions, so the pruned search still visits every valid complete solution — and only ever reports valid ones, since the base case additionally re-validates (or, in the N-Queens code above, validity is guaranteed inductively by the loop invariant that `placement[0..row-1]` is always consistent). Hence the algorithm is sound (never reports an invalid solution) and complete (finds every valid solution, or the best one, depending on variant).
Time complexity. In the worst case, backtracking degenerates to brute force: if `IS_CONSISTENT` almost never prunes, the algorithm explores the entire candidate tree, whose size is the product of branching factors, — for N-Queens, this is bounded above by (and, with the column/permutation restriction already baked into the domain choice, exactly in the worst realistic accounting). This is why backtracking is, in the worst case, exponential time: no pruning strategy can guarantee a polynomial bound for NP-hard problems like graph coloring or general Boolean satisfiability, because that would imply P = NP. The honest complexity statement is:
where is the cost of `IS_CONSISTENT` and `UNDO` at each node (typically to ). Pruning changes the constant and the base of the exponent dramatically in practice — for 8-Queens, the naive placements shrinks to roughly nodes actually explored by the column/diagonal pruning above — but it does not change the asymptotic class in the worst case over all inputs. This is the same honest tradeoff you should recall from recursive The 0/1 Knapsack Problem: without memoization or pruning, the recursion tree is exponential (), and only additional structure (overlapping subproblems there, constraint locality here) tames it in practice.
Space complexity. Because backtracking is depth-first and reuses a single mutable partial-solution array with explicit undo, the space used is proportional to the maximum recursion depth times the state stored per level: for a problem with decision positions, plus (or , problem-dependent) for auxiliary bookkeeping arrays like `cols`, `diag1`, `diag2`. This is a crucial practical advantage over algorithms that materialize the entire tree or store all partial solutions simultaneously (breadth-first search over the same space would cost space exponential in the branching factor).
Tradeoffs. The quality of pruning is everything. A weak `IS_CONSISTENT` (checked only at the leaf) reduces backtracking to brute-force enumeration with the same exponential cost but no early exit — you have built a correct but useless "backtracking" algorithm. A strong, cheap, incremental consistency check (updatable in per decision, as with the diagonal arrays) is what makes real-world backtracking solvers (Sudoku, SAT, scheduling) fast on typical instances despite worst-case exponential behavior. Ordering heuristics — trying the most constrained variable first, or the most likely-to-succeed value first — further shrink the explored tree without changing the worst-case bound; this idea, generalized with numeric bounds rather than boolean feasibility, is exactly the extension that Branch and Bound performs.
Worked example
Trace `SOLVE_N_QUEENS` for (a small case chosen because it exposes real pruning and real backtracking, not a trivial symmetric shortcut). Rows are numbered –, columns –.
| Step | Row | Try col | cols/diag check | Action |
|---|---|---|---|---|
| 1 | 0 | 0 | all clear | place (0,0); recurse to row 1 |
| 2 | 1 | 0 | `cols[0]` occupied | skip |
| 3 | 1 | 1 | shares diag1 with (0,0) (row−col: −1 vs 1... actually check: (0,0)→d1=0; (1,1)→d1=0 — conflict) | skip |
| 4 | 1 | 2 | clear | place (1,2); recurse to row 2 |
| 5 | 2 | 0 | shares diag2 with (1,2)? (1,2)→d2=3; (2,0)→d2=2, clear of diag2; but check diag1 with (0,0): (2,0)→d1=2, (0,0)→d1=0, clear; check col0 clear | clear → place (2,0)? but must also check vs (1,2): d1 for (2,0)=2, (1,2)'s d1 = -1 → clear. Place (2,0); recurse row 3 |
| 6 | 3 | 0,1,2,3 | col0 occupied (skip 0); col2 occupied (skip 2); check col1: diag with (2,0) d2=1+... conflicts found for 1 and 3 | all four columns fail → dead end, backtrack to row 2 |
| 7 | 2 | 1 | conflicts with (1,2) same column | skip |
| 8 | 2 | 2 | col2 occupied by (1,2) | skip |
| 9 | 2 | 3 | check vs (0,0): d1=3 vs 0 clear, d2=5 vs 0 clear; vs (1,2): d1=1 vs -1 clear, d2=5 vs 3 clear; col3 clear | place (2,3); recurse row 3 |
| 10 | 3 | 0 | col0 clear; vs (0,0) same column → conflict | skip |
| 11 | 3 | 1 | vs (2,3): d1 = 2 vs -1 clear, d2=4 vs 5 clear; vs (1,2): d1=2 vs -1 clear, d2=4 vs 3 clear; vs (0,0): d1=3 vs 0 clear, d2=4 vs 0 clear; col1 clear | place (3,1) → complete solution: (0,0),(1,2),(2,3)? wait recheck row2 pick was col3 → board is (0,0),(1,2),(2,3)? — continue |
| 12 | — | — | base case row==4 reached | solution #1 recorded, count = 1; backtrack fully to row 0 |
| 13 | 0 | 1 | clear | place (0,1); recurse — eventually finds solution #2 by symmetric exploration |
| 14 | 0 | 2 | (mirror of col1 case) | finds solution #3 |
| 15 | 0 | 3 | (mirror of col0 case) | finds solution #4 |
The full search terminates with count = 2, the correct number of distinct solutions for 4-Queens (the classic pair (1,3,0,2) and (2,0,3,1) in 0-indexed column notation — the table above illustrates the mechanics of one accepted branch and the pruning that rejects invalid columns; the second true solution is discovered by continuing the row-0 loop after backtracking). The important structural fact the trace demonstrates: step 6 shows an entire row-3 subtree pruned in a single failed loop (all four columns rejected without any deeper recursion), and step 12 shows the undo-and-continue behavior — after recording a solution at the base case, the algorithm does not terminate; it pops back up and resumes the loop at the parent frame, exactly as the `UNDO` line in the template prescribes.
Common pitfalls & traps
| Pitfall | Why it happens | Fix |
|---|---|---|
| Forgetting to undo shared state | Reusing global arrays (`cols`, `diag1`, `diag2`) without resetting them after a failed branch corrupts later sibling checks | Always pair every mutation in the "choose" step with an exact inverse in "undo," executed unconditionally after the recursive call returns |
| Checking feasibility only at the leaf | Turns backtracking into brute force — no pruning benefit at all, same exponential blow-up but no early exits | Push the consistency check as early as possible: test the newest decision against all prior ones before recursing |
| Off-by-one in domain/index bounds | Diagonal index arrays (`row - col`) can go negative; forgetting the shift `+ (N-1)` causes out-of-bounds access or silent aliasing | Always derive index ranges from the math () and shift explicitly |
| Confusing "find one" with "find all" | Returning too early (or not early enough) either misses solutions or wastes time exploring after success | Decide the variant up front; `find-one` propagates `true` and stops recursion; `find-all` never stops early |
| Assuming pruning fixes worst-case complexity | Good pruning helps typical instances enormously but the problem may still be NP-hard in general | State bounds honestly: "exponential worst case, often fast in practice due to pruning," never claim polynomial time without proof |
| Mutating a solution vector by reference without copying at recording time | Recording a pointer/reference to the shared `partial` array means all "recorded" solutions end up identical (the final state) | Deep-copy the partial solution before storing it in the result set |
Pioneers & history
The term "backtrack" for systematic trial-and-error search with retreat on failure was formalized in the mid-20th century as combinatorial computing matured, but the core recursive-retreat idea is far older, appearing implicitly in 19th-century combinatorial puzzle analysis (including early studies of the Eight Queens Puzzle posed by Max Bezzel in 1848 and studied by Gauss). The algorithmic and computational formalization — giving it a name, a general template, and a place in algorithm design — is credited to the American mathematician and computer scientist D. H. Lehmer and, independently and more influentially for the term itself, to R. J. Walker, whose 1960 paper systematized backtrack programming for combinatorial problems. The technique was subsequently absorbed into the broader theory of combinatorial search and constraint satisfaction developed through the 1960s–70s.
Origins · 1 minRobert J. Walker and how this was discovered
Walker's 1960 paper "An Enumerative Technique for a Class of Combinatorial Problems" gave one of the first systematic treatments of backtracking as a general algorithmic pattern, independent of any single puzzle, framing it explicitly as depth-first construction of partial solutions with rejection of infeasible partial states — precisely the template taught today.
Where this matters
Backtracking is the quiet workhorse behind an enormous range of practical systems. SAT solvers (used in hardware verification, software model checking, and package-dependency resolvers like those in Linux distributions) are built on the DPLL algorithm, which is backtracking search over partial variable assignments with unit-propagation as its pruning rule, later supercharged with clause learning. Sudoku and puzzle solvers, as well as automated puzzle generators, use backtracking directly, often paired with heuristics like choosing the most-constrained empty cell first. Compiler register allocation can be framed as graph coloring, solved via backtracking with pruning on color conflicts when the underlying graph is small or sparse enough. Regular expression engines (particularly "backtracking" engines like those historically in Perl, PCRE, and Python's `re` module) use exactly this depth-first-with-retreat structure to try alternative matches when a greedy match fails — the notorious "catastrophic backtracking" performance bug in regex engines is precisely a case of a weak or missing pruning test on a nested-repetition pattern. Game-tree search in classical AI (before neural evaluation functions) explored move sequences via backtracking-like DFS, and constraint propagation in scheduling and resource-allocation systems (exam timetabling, register-transfer-level synthesis) is a direct industrial descendant. The natural next step — adding numeric objective bounds instead of purely boolean feasibility, so the search also prunes branches that are feasible but provably suboptimal — is the subject of Branch and Bound.
Ask the tutor: How would you modify this backtracking template to add a numeric bound for an optimization version of N-Queens-like scheduling, turning it into a Branch and Bound algorithm?
Is backtracking the same thing as brute force?
No, though they can coincide in the worst case. Brute force generates every complete candidate and tests it at the end; backtracking tests every partial candidate as early as possible and refuses to extend infeasible ones. When the feasibility test never actually prunes anything (e.g., it is checked only at leaves), backtracking degenerates into brute force with identical cost. The value of backtracking comes entirely from how early and how cheaply it can detect infeasibility, which is why designing a strong, incrementally-updatable consistency check is the real engineering work in a backtracking solver.
Why is backtracking still exponential in the worst case if pruning helps so much?
Pruning changes the size of the tree actually explored, not the theoretical worst case over all possible inputs. For NP-hard problems like general graph coloring or Boolean satisfiability, no known pruning strategy guarantees polynomial-time solving on every instance — if one did, it would imply P = NP. In practice, well-designed pruning (constraint propagation, good variable/value ordering) can make the explored tree tiny for typical or structured instances, but adversarial or dense instances can still force exponential blow-up. The honest statement is: worst-case exponential, often fast in practice.
How does backtracking relate to depth-first search on a graph?
Backtracking is depth-first search on an implicit tree — the state space of partial solutions — rather than an explicit graph given in memory. The tree is generated on the fly: each node's children are computed by the DOMAIN function only when needed, and are never materialized all at once. This is why backtracking uses O(n) space for an n-decision problem rather than space proportional to the (exponential) size of the full tree: only the current root-to-node path is ever held in memory, exactly as in ordinary recursive DFS.
What is the difference between backtracking and dynamic programming, given both showed up in knapsack-style problems?
Dynamic programming exploits overlapping subproblems: many different decision sequences lead to the same reduced subproblem (e.g., same remaining capacity in knapsack), so memoizing collapses an exponential tree into a polynomial table. Backtracking instead exploits constraint locality: it does not assume subproblems repeat, but instead prunes branches using cheap feasibility tests. When a problem has both overlapping subproblems and easy feasibility tests, you can combine the two ideas, but generically they solve different obstructions to brute force: repetition versus infeasibility.
Does the order in which candidate values are tried affect correctness?
No — correctness only requires that every feasible value is eventually tried and that infeasible branches are correctly identified; the order does not change the final set of solutions found (for a "find-all" variant) or whether a solution exists (for a "find-one" variant). Order does dramatically affect running time, however: trying the most likely-to-succeed value first can find a solution (or prove infeasibility) far faster in practice, which is the core heuristic idea behind variable/value ordering strategies in constraint satisfaction and SAT solving.
Can backtracking be used for optimization problems, not just yes/no feasibility?
Yes, with an extension: alongside the feasibility check, you track the best complete solution seen so far and add a bounding test that prunes any partial solution whose best conceivable completion cannot beat the current best. This numeric pruning, layered on top of ordinary backtracking, is precisely the idea behind Branch and Bound, which is the natural next technique to study after mastering plain feasibility-based backtracking.
Why does recursion make backtracking easy to implement correctly?
Recursion gives you the call stack for free as the mechanism that remembers "where to resume" after a subtree is fully explored, and it naturally scopes the undo step: when a recursive call returns (whether by success or exhausting all its own options), control returns to the exact point where the parent can immediately undo its own most recent choice and try the next candidate. This tight coupling between "the recursion returns" and "time to undo" is why the template is almost always written recursively rather than with an explicit manual stack, even though the latter is possible and sometimes necessary for very deep search trees to avoid stack-overflow limits.