Structs & Data Structures · intermediate · ~14 min
Place, recurse, and undo.
Backtracking is recursion that makes a choice, explores the consequences, and then undoes the choice before trying the next one. N-Queens is the canonical example: place a queen in the current row, recurse to the next row, and on return remove her so the next column can be tried. What makes it fast enough to be practical is pruning — never recursing into a placement that is already known to be attacked. The bitmask formulation makes that check a single AND, and the undo free, because the state is passed by value rather than mutated.
Backtracking is the general technique for constraint-satisfaction problems: Sudoku, graph colouring, scheduling with conflicts, crossword filling, and puzzle solvers of all kinds. The place-recurse-undo skeleton is identical across all of them, so learning it once transfers widely. And pruning is what separates a solver that finishes from one that does not — the difference is often many orders of magnitude.
The skeleton. For each candidate: if it is valid, apply it, recurse, then undo it. The undo is what makes the search a tree rather than a one-way walk.
Pruning is the point. Testing a placement before recursing cuts an entire subtree. Without pruning, N-Queens on an 8x8 board would explore an astronomically larger space; with it, the search finishes instantly.
Bitmask state. Represent the threatened columns and the two diagonal families as three integers. A queen at column c sets bit c in cols; the diagonals shift by one each row, so passing (d1 | bit) << 1 and (d2 | bit) >> 1 to the next row automatically expresses "this diagonal threat moves over by one".
Pass-by-value is the undo. Because the masks are passed as arguments rather than mutated in place, returning from a call restores the previous state for free — no explicit cleanup and no possibility of forgetting it.
The available-squares trick. free = ~(cols | d1 | d2) & full gives every safe column at once; iterating with bit = free & -free (isolate the lowest set bit) and free &= free - 1 (strip it) reuses exactly the bit-manipulation idioms from earlier in the curriculum.
Complexity. Still exponential in the worst case — backtracking prunes, it does not change the class.
/* count solutions; cols/d1/d2 are the threatened columns and diagonals */
static int solve(int n, int row, int cols, int d1, int d2) {
if (row == n) return 1; /* all rows placed - one solution */
int full = (1 << n) - 1;
int free_sq = ~(cols | d1 | d2) & full; /* every safe column at once */
int count = 0;
while (free_sq) {
int bit = free_sq & -free_sq; /* isolate lowest safe column */
free_sq &= free_sq - 1; /* strip it for the next iteration */
/* state is passed BY VALUE, so the undo is automatic on return */
count += solve(n, row + 1,
cols | bit,
(d1 | bit) << 1, /* diagonals shift as we move down */
(d2 | bit) >> 1);
}
return count;
}
/* call: solve(n, 0, 0, 0, 0) */
Key points:
(1 << n) - 1 requires n < 32; guard it, since 1 << 32 is undefined.Backtracking explores a tree of choices: place a queen, recurse to the next row, and undo the placement to try the next column. A safe predicate prunes attacked squares. This place-recurse-undo pattern solves constraint puzzles in general.
#include <stdio.h>
static int solve(int n,int row,int cols,int d1,int d2){
if(row==n) return 1;
int c=0;
for(int col=0;col<n;col++){ int cb=1<<col,b1=1<<(row+col),b2=1<<(row-col+n-1);
if((cols&cb)||(d1&b1)||(d2&b2)) continue;
c+=solve(n,row+1,cols|cb,d1|b1,d2|b2); }
return c;
}
int main(void){
printf("solutions to the N-Queens puzzle:\n");
for(int n=1;n<=8;n++) printf(" %d-queens: %d\n", n, solve(n,0,0,0,0));
return 0;
}
| Step | Line | What happens |
|---|---|---|
| 1 | solve(4, 0, 0, 0, 0) |
Row 0, nothing threatened, so free_sq is all four columns. |
| 2 | bit = free_sq & -free_sq |
Isolates column 0 as the first candidate. |
| 3 | recurse | Row 1 receives cols = 0001, d1 = 0010, d2 = 0000 — the diagonals already shifted. |
| 4 | row 1 | free_sq now excludes columns 0, 1 — pruning removed them without ever recursing. |
| 5 | dead end | If free_sq is 0 the loop body never runs, the call returns 0, and the caller simply tries its next bit — that return is the backtrack. |
| 6 | row == n |
A full placement; returns 1. For n = 4 the total is 2; for n = 8 it is 92. |
Not undoing state on return; incomplete attack checks (missing diagonals).
Compiler errors and warnings:
warning: left shift count >= width of type when n reaches 32 — guard the board size.-Wsign-compare or unexpected sign extension if the masks are signed and bit 31 is touched; keep n small or use unsigned.Runtime symptoms:
full is miscomputed so phantom columns appear.d1 shifts left, d2 shifts right as the row advances.free_sq &= free_sq - 1, so the same column is tried forever.n = 4 but wrong for n = 8. Usually the diagonal handling; verify against the known sequence 1, 0, 0, 2, 10, 4, 40, 92.Technique: check against the known solution counts for n = 1..8. Any deviation pinpoints which constraint is mishandled.
(1 << n) - 1 is undefined for n >= 32; validate n before use. This is the same undefined-shift hazard from the bit-manipulation track.(d1 | bit) << 1 on a signed int can overflow into the sign bit for large n. Use unsigned masks if you intend to support wide boards.n, so the stack is safe — backtracking's cost is time, not memory.return path must undo the placement; an early return that skips the undo corrupts all later branches. The by-value approach removes that entire class of bug.n from untrusted input without a cap is a denial-of-service risk.Concrete uses: Sudoku and puzzle solvers, graph colouring, exam and shift timetabling with conflict constraints, register allocation in compilers, crossword and word-fill generation, and SAT-style search. The place-recurse-undo skeleton is shared by all of them; only the validity test changes.
Professional best practices:
Beginner:
Intermediate:
1. (Beginner) Count N-Queens solutions. Implement solve(n, 0, 0, 0, 0) with bitmasks. Example: n = 4 -> 2; n = 8 -> 92. Concepts: place-recurse-undo, pruning.
2. (Beginner) Verify the sequence. Print solution counts for n = 1..8 and compare with 1, 0, 0, 2, 10, 4, 40, 92. Concepts: validating against a known oracle.
3. (Intermediate) Print one solution. Modify the search to record the column chosen per row and print the first complete board. Concepts: capturing state during backtracking.
4. (Intermediate) Array-based version. Reimplement with an explicit int cols[n] array and a safe() check, making the undo explicit. Requirements: it must agree with the bitmask version. Concepts: explicit undo, why by-value is safer.
Backtracking is choose, recurse, undo — and the undo is what turns a walk into a search tree. Pruning invalid choices before recursing is what makes it finish: it removes whole subtrees rather than exploring and rejecting them. The bitmask formulation of N-Queens keeps threatened columns and both diagonal families in three integers, shifting the diagonals by one per row, and because that state is passed by value the undo happens automatically on return — eliminating the single most common backtracking bug. Guard the board size against undefined shifts, and remember the algorithm is still exponential.