1. Introduction & Motivation
Some problems cannot be solved by a formula or a single greedy pass - they require searching a space of candidate solutions and rejecting the ones that violate a constraint. Backtracking is the disciplined way to run that search: build a candidate one decision at a time, check the constraint as early as possible, and undo the most recent decision the instant it fails. Done well, backtracking still explores an exponential space, but it prunes entire branches before they are fully built, which is the difference between an algorithm that finishes and one that never returns.
This lesson is the last of six that split apart a single overloaded guide to graphs, dynamic programming, greedy algorithms, and backtracking. Where greedy algorithms commit to one choice per step and never look back, backtracking keeps every option open and reverses course the moment a choice proves wrong. That reversal - make_choice, recurse, undo_choice - is the one idea this entire lesson builds on, from generating permutations to solving N-Queens with bitmask pruning.
2. Prerequisites
Backtracking is recursion plus one extra discipline: undo what you just did before trying the next option. If recursive call stacks, base cases, and tracing a call tree by hand are not yet comfortable, work through Part 2: Recursion Complete Guide first - everything below assumes you can reason about a recursive call tree without writing it out.
This lesson also assumes you can tell backtracking apart from the algorithm family it is most often confused with: greedy algorithms. Part 16: Greedy Proofs & Scheduling proves when committing to the locally best choice is safe and never needs to be undone. Backtracking is what you reach for when no such proof exists - when the only way to know a choice was wrong is to keep going and eventually fail.
3. The Backtracking Template
Backtracking is a systematic way to explore all possible solutions by building candidates incrementally and abandoning ("backtracking") candidates that fail to satisfy constraints.
Backtracking Template
def backtrack(candidate):
if is_solution(candidate):
output(candidate)
return
for choice in choices:
if is_valid(choice):
make_choice(choice)
backtrack(candidate)
undo_choice(choice) # Backtrack
4. Permutations
Generate all possible orderings of a given set. At each position, try every unused element, recurse to fill the next position, then undo the choice (backtrack). The decision tree has $n!$ leaves (one per permutation). Key constraint: use a "used" boolean array to avoid placing the same element twice. For duplicates, sort first and skip adjacent identical unused elements to avoid duplicate permutations.
5. Subsets
Generate all $2^n$ subsets (the power set) of a given array. At each element, make a binary choice: include it or exclude it. The recursion tree branches two ways at each level, giving $2^n$ total subsets. Implementation uses an index $i$ to avoid revisiting earlier elements (since subset order doesn't matter). This is the foundation for many search problems — Subsets II (with duplicates) adds a skip condition for consecutive identical values.
6. Combination Sum
Find all unique combinations that sum to a target. Unlike subsets (which enumerate all), here we prune branches where the running sum exceeds the target — this is backtracking's power over brute force. The key decision at each step: which candidate to add next (starting from index $i$ to avoid reusing earlier positions). In the "reuse allowed" variant, recurse with the same index $i$ (not $i+1$); in "use once" variants, advance to $i+1$.
7. N-Queens
Place $N$ queens on an $N \times N$ chessboard so no two queens attack each other (no shared row, column, or diagonal). This is the classic constraint-satisfaction backtracking problem. Place queens row by row; for each row, try each column and check if it's safe (no queen in the same column, or either diagonal). Use sets or boolean arrays to track occupied columns and diagonals. The diagonal trick: cells on the same diagonal have equal $row - col$ (main) or $row + col$ (anti-diagonal).
8. Constraint Propagation: Pruning Smarter
The N-Queens baseline above is correct, but it discovers conflicts late: for each candidate column it asks "is this column, or either diagonal, already attacked?" one set lookup at a time, and it only rejects a choice once the loop actually reaches it. Two ideas make the search itself smarter, not just the constraint check.
Forward Checking
Plain backtracking discovers a conflict by recursing all the way down until some future assignment has no legal value left, then unwinding. Forward checking does the check the moment a variable is assigned instead: as soon as a queen is placed, its column and both diagonals are eliminated from the domain of every future row immediately, so a row that has just lost its last legal column is caught right away - before the recursion wastes time descending into it. The information does not wait to be rediscovered by brute-force recursion; it propagates forward.
The Minimum Remaining Values (MRV) Heuristic
Backtracking still has to decide which variable to branch on next. Scanning rows top-to-bottom is arbitrary - it only works for N-Queens because every row is symmetric. In general, the MRV heuristic says: branch on the most-constrained variable first, the one with the fewest legal values remaining in its domain. A variable with one legal value left (or zero) fails immediately, so finding it early kills a doomed branch before the search wastes time on easier, less-constrained variables. MRV pays off most in problems where variables are not identical, such as Sudoku's 81 cells - see the Hard exercise below.
Applying Both to N-Queens: Bitmask Attack Tracking
The baseline solution tracks attacked columns and diagonals with three sets, each membership test a hash lookup, and each placement or removal touches three separate insert/erase calls. A bitmask representation makes both the check and the update a single machine-word operation, and because setting a queen's column and diagonal bits the moment it is placed - then clearing them the moment it is removed - is itself forward checking, the domain of every future row is always current without a separate propagation pass:
cols- bit $c$ set means column $c$ is attacked by a queen already placed.diag1- bit $c$ set means the "/" diagonal through the current row and column $c$ is attacked; shifting it left by one bit as the row advances keeps it aligned with the next row's columns.diag2- bit $c$ set means the "\" diagonal through the current row and column $c$ is attacked; shifting it right by one bit keeps it aligned as the row advances.
available = ((1 << n) - 1) & ~(cols | diag1 | diag2) computes every legal column for the current row in one instruction - the forward-checked domain of "columns no placed queen has eliminated yet" - instead of looping over columns and testing each against three sets. Extracting the lowest set bit with available & (-available) and clearing it walks the legal columns one at a time. This is the same bitmask machinery covered in the bit manipulation lesson - isolating the lowest set bit, shifting masks as a frontier advances, and using bitwise AND/OR/NOT to represent set membership are exactly the techniques this optimization borrows.
Each recursive call now does a fixed number of word-sized operations regardless of $n$, so the per-node cost drops from $O(n)$ set operations to $O(1)$ bitwise operations. The number of nodes explored - still bounded by $O(n!)$ in the worst case - does not change; what changes is the constant factor per node, which is why the bitmask version is the one used once $n$ is large enough to matter.
9. Exercises and Mastery
Easy
- Solve LeetCode 17: Letter Combinations of a Phone Number. Backtrack over digit positions, mapping each digit to its letters and appending one character at a time, then undoing it before trying the next letter.
Medium
- Solve LeetCode 79: Word Search. Backtrack over grid cells in four directions, marking each visited cell before the recursive call and unmarking it afterward, so no cell is reused twice within the same path.
Hard
- Solve LeetCode 37: Sudoku Solver by applying this lesson's ideas rather than plain brute-force backtracking: maintain the set of candidate digits still legal for every empty cell (forward checking), and always fill the cell with the fewest remaining candidates next (MRV) instead of scanning cells in fixed row-major order.
Review rhythm: tomorrow, reconstruct the generic backtracking template and the bitmask N-Queens trick without notes; next week, implement forward checking and MRV for Sudoku from the exercise above; several weeks later, find the same "eliminate now, branch on the most-constrained choice" idea in a new constraint-satisfaction problem. Finally, teach the difference between a plain backtracking check and constraint propagation to someone who only knows the generic template.
10. Summary Cheat Sheet
- Backtracking builds candidates incrementally and undoes any choice that violates a constraint before trying the next one.
- The generic template is: check for a solution, try each choice, recurse, undo.
- Permutations, subsets, and combination sum are the three foundational backtracking shapes, covering orderings, power sets, and pruned sums.
- N-Queens is the classic constraint-satisfaction backtracking problem, tracking attacked columns and both diagonals.
- Forward checking eliminates now-invalid values from future variables' domains the moment a variable is assigned, catching conflicts before brute-force recursion reaches them.
- The MRV heuristic branches on the most-constrained variable first, so failing branches die early instead of late.
- Bitmasks turn N-Queens' attack checks from $O(n)$ set lookups into $O(1)$ bitwise operations, updated incrementally as queens are placed and removed.
- The same domain-pruning ideas power harder CSPs like Sudoku, where forward checking and MRV matter far more than in symmetric problems like N-Queens.