On this page
Description
Sudoku, N-Queens, map coloring and timetabling share one shape. There's a set of unknowns, such as cells, rows, regions or time slots. Each needs a value, and rules forbid certain combinations. Trying every complete assignment is hopeless: 8 queens, one per row, already have 16,777,216. Constraint-satisfaction backtracking fills unknowns one at a time and rejects a value the moment it breaks a rule. Good bookkeeping and a good choice of which unknown to fill next make it fast on realistic inputs.
We'll go from simple to more technical:
- The pattern: place, check the rules immediately, recurse, undo, using O(1) lookups.
- A second flavor: fill the most constrained unknown first.
- Where constraint solving runs in production (scheduling, configuration, puzzles).
- What breaks it: late checks, bookkeeping that isn't undone, and slow checks.
The approach
Java
boolean solve(State state, int position) {
if (position == totalPositions) return true; // every unknown filled legally
for (int value : candidatesFor(position)) {
if (!isAllowed(state, position, value)) continue; // check BEFORE going deeper
place(state, position, value);
if (solve(state, position + 1)) return true; // stop at the first full solution
unplace(state, position, value); // undo - try the next value
}
return false; // nothing fits here: the caller backtracks
}
Python
def solve(state: State, position: int) -> bool:
if position == total_positions:
return True # every unknown filled legally
for value in candidates_for(position):
if not is_allowed(state, position, value):
continue # check BEFORE going deeper
place(state, position, value)
if solve(state, position + 1):
return True # stop at the first full solution
unplace(state, position, value) # undo - try the next value
return False # nothing fits here: the caller backtracks
The check comes before the recursive call, so a partial assignment that already breaks a rule is never extended. For 8 queens, placing row by row and skipping attacked columns and diagonals places only 2,056 queens in total to find all 92 solutions. Returning true up the stack stops at the first solution (Sudoku); counting instead of returning explores everything (N-Queens counts).
The checks have to be cheap. Keep lookup structures in step with the board - which digits each row, column and 3 x 3 box already has; which columns and diagonals already hold a queen - so isAllowed is O(1). Cells on the same "" diagonal share r - c, cells on the same "/" diagonal share r + c, so two boolean arrays of size 2n - 1 cover all diagonals.
A second flavor: choose the most constrained unknown first
The order in which unknowns are filled doesn't change what's found, but it hugely changes how much is explored. Filling the empty cell with the fewest remaining candidates first (the "most constrained variable" heuristic) fails fast on dead ends and makes forced moves immediately:
Java
int bestR = -1, bestC = -1, fewest = 10;
for (int r = 0; r < 9; r++)
for (int c = 0; c < 9; c++)
if (board[r][c] == 0) {
int options = countAllowedDigits(r, c);
if (options < fewest) { fewest = options; bestR = r; bestC = c; }
}
if (bestR == -1) return true; // no empty cell left: solved
// try each allowed digit at (bestR, bestC), recurse, undo
Python
def solve(board: list[list[int]]) -> bool:
best, fewest = None, 10
for r in range(9):
for c in range(9):
if board[r][c] == 0:
options = count_allowed_digits(r, c)
if options < fewest:
best, fewest = (r, c), options
if best is None:
return True # no empty cell left: solved
# try each allowed digit at best, recurse, undo
On the classic sample puzzle, row-by-row filling makes 4,208 placements; most-constrained-first makes 51. A cell with zero options ends the branch at once, instead of after filling everything before it.
Where you'll see it
Scheduling (exam timetables, nurse rosters, sports fixtures), resource placement (VMs onto hosts respecting capacity and anti-affinity) and configuration (package version resolution) are constraint satisfaction. Production solvers - SAT and CP-SAT solvers, dependency resolvers - are this backtracking loop plus propagation (removing values ruled out by each placement), smart variable ordering and learning from conflicts.
Which tool for which job?
| You need | Use |
|---|---|
| Any CSP (Sudoku, N-Queens, coloring) | place, check at once, recurse, undo |
| Fast rule checks | used-value sets or bitmasks per row, column and box |
| Fewer dead ends | the most constrained unknown first |
| Even fewer guesses | propagation: fill single-candidate unknowns, fail on zero |
| Symmetric puzzles | symmetry breaking (e.g. N-Queens' first row in the left half) |
| Large real-world problems | a SAT or constraint-programming solver |
Common mistakes
| Mistake | What happens | Do this instead |
|---|---|---|
| Checking only complete assignments | 16.8 million for 8 queens | reject a value as soon as it breaks a rule |
| Undoing the board but not the bookkeeping | valid placements are rejected; "no solution" | undo every structure you updated |
| Scanning the board for each check | every candidate costs O(n) | O(1) sets or bitmasks |
| Filling unknowns in a fixed order | 4,208 placements instead of 51 on a sample Sudoku | most constrained first |
Check yourself
Try to answer before opening each one - recalling beats re-reading.
1. Why check a constraint before recursing rather than when the assignment is complete?
Show answer
A partial assignment that already breaks a rule can never become valid, so every extension of it is wasted work. Checking at placement abandons the whole subtree at once: 8 queens needs 2,056 placements that way, versus 16,777,216 complete one-per-row assignments to check at the end.
2. A Sudoku solver clears the cell when it backtracks but leaves the digit marked in its row/column/box sets. What happens?
Show answer
The digit stays "used" in those sets, so later branches can't place it where it's actually legal. The solver rejects valid placements and can fail to find any solution - on the sample puzzle it reports none. Every undo must restore all the bookkeeping the placement changed.
3. How do two arrays of size 2n - 1 track every diagonal for N-Queens?
Show answer
On a "" diagonal, r - c is constant (ranging from -(n - 1) to n - 1, shifted by n - 1 to index an array); on a "/" diagonal, r + c is constant (0 to 2n - 2). A queen at (r, c) marks both entries, so "is this diagonal attacked?" is one array lookup.
Key takeaways
- Fill unknowns one at a time; check each value against the rules before recursing, and undo on the way back.
- Keep O(1) lookup structures (per row/column/box, per column/diagonal) in step with every placement and undo.
- Return true to stop at the first solution; count to enumerate all.
- Filling the most constrained unknown first often cuts the search dramatically (4,208 to 51 placements on one Sudoku).
- The worst case stays exponential - CSPs are NP-complete in general.