Sudoku Solver
Fill empty cells one by one with digits that do not conflict in their row, column, or 3×3 box, backtracking on dead ends.
Overview
Sudoku is N-Queens with three constraint families instead of four attack lines: each digit 1–9 appears once per row, once per column, and once per 3×3 box. Keep rows[r][d], cols[c][d], boxes[b][d] occupancy tables so "is digit d legal at (r, c)" is O(1).
A plain left-to-right scan works for typical puzzles, but the minimum remaining values (MRV) heuristic — always fill the empty cell with the fewest legal digits — prunes dramatically on hard boards and is what a good interviewer wants to hear about.
Intuition
A mental model before the formal terms.
Fill the grid the way a person does when guessing: write a candidate digit in pencil, keep going, and if you reach a cell with no legal digit, erase back to the last guess and try the next one.
Picking the most constrained cell first is like solving the corner of a jigsaw before the sky: fewer options means fewer wasted guesses and earlier detection of contradictions.
How it works
- Initialize
rows,cols,boxesfrom the given digits; box index is(r // 3) * 3 + c // 3. - Collect the empty cells. Choose the next cell to fill (first empty, or the one with the fewest legal digits under MRV).
- Base case: no empty cells remain — the board is solved; return
true. - For each digit
dlegal at that cell: place it, update the three tables, recurse; ontruepropagate success, otherwise undo and try the next digit. - If no digit works, return
falseso the caller backtracks.
Why it works
The three tables are exactly the Sudoku rules, so a placement passing the check never violates a rule with already-placed digits; conflicts are monotone, so pruning on any violation is sound.
Depth-first search over cell assignments is exhaustive: if a solution exists, some branch places each of its digits in turn, and each such placement passes the check.
MRV does not change correctness, only order — but a cell with 0 legal digits is detected immediately, and cells with 1 legal digit are forced moves, so the effective branching factor drops close to 1.
Recognition
How to tell a problem wants this.
- Fill a grid subject to "each value once per row / column / region" rules.
- Fixed tiny board (9×9) with a guarantee that a solution exists — exhaustive search is expected.
- Any exact-cover-flavored puzzle (Kakuro, KenKen) for which you would otherwise reach for Dancing Links.
Interactive visualization
Play, step, change the input. ← → and space work too.
1solve():2 find next empty cell (r, c); if none: return true3 for v in 1 .. N:4 if valid(r, c, v):5 board[r][c] = v6 if solve(): return true7 board[r][c] = 0 // backtrack8 return falsePseudocode
1solve():2 cell = pick empty cell (first, or fewest candidates)3 if none: return true4 for d in 1..9:5 if d in rows[r] or cols[c] or boxes[box(r,c)]: continue6 place d; mark tables7 if solve(): return true8 remove d; unmark tables9 return falseImplementations
1def solve_sudoku(board: list[list[str]]) -> bool:2 """Solves in place; '.' marks empty cells. Returns True if solvable."""31 · Occupancy bitmasks from the given board4 rows = [0] * 95 cols = [0] * 96 boxes = [0] * 97 empties: list[tuple[int, int]] = []8 for r in range(9):9 for c in range(9):10 ch = board[r][c]11 if ch == ".":12 empties.append((r, c))13 else:14 bit = 1 << int(ch)15 rows[r] |= bit16 cols[c] |= bit17 boxes[(r // 3) * 3 + c // 3] |= bit18 192 · Legal-digit mask for a cell20 def used_mask(r: int, c: int) -> int:21 return rows[r] | cols[c] | boxes[(r // 3) * 3 + c // 3]22 23 def fill() -> bool:24 if not empties:25 return True263 · MRV: fill the most constrained cell first27 best = min(range(len(empties)),28 key=lambda i: 9 - (used_mask(*empties[i]) & 0x3FE).bit_count())29 empties[best], empties[-1] = empties[-1], empties[best]30 r, c = cell = empties.pop()31 b = (r // 3) * 3 + c // 3324 · Try each legal digit, recurse, undo33 for d in range(1, 10):34 bit = 1 << d35 if used_mask(r, c) & bit:36 continue37 board[r][c] = str(d)38 rows[r] |= bit39 cols[c] |= bit40 boxes[b] |= bit41 if fill():42 return True43 rows[r] ^= bit44 cols[c] ^= bit45 boxes[b] ^= bit46 board[r][c] = "."475 · Dead end: restore the cell list and backtrack48 empties.append(cell)49 empties[best], empties[-1] = empties[-1], empties[best]50 return False51 52 return fill()- Section 1 builds integer bitmasks per row, column, and box; Python ints are arbitrary precision, so a 10-bit mask is just a small int.
used_maskORs the three masks; the nestedfillclosure mutates the enclosing lists directly, so no parameters are threaded through.- Section 3 uses
min(range(len(empties)), key=...)withint.bit_count()to find the cell with the fewest legal digits (MRV). r, c = cell = empties.pop()binds the tuple and unpacks it in one statement after the swap-to-end.- Section 4 tries each legal digit: set the bits with
|=, recurse, and undo with^=plus restoring the "." marker. - Section 5 reinserts the cell and reverses the swap so every sibling candidate sees the same
emptiesorder.
Recursion depth is at most 81, far below CPython's default limit of 1000 — no sys.setrecursionlimit needed here.
int.bit_count()needs Python 3.10+; on older versions usebin(x).count("1").- Sets of used digits (
rows[r] = set()) read more naturally but are several times slower than int bitmasks in CPython. - The closure over
rows/cols/boxesworks because the code only mutates elements; rebinding the names would neednonlocal.
- Wrong box index —
(r // 3) * 3 + c // 3, notr // 3 + c // 3. - Using
/instead of//so the box index becomes a float and raisesTypeErroron indexing. - Forgetting to restore
board[r][c] = "."on failure, leaving stale digits that corrupt later checks.
- Popcount: C++ has
__builtin_popcount(or C++20std::popcount), Python 3.10+ hasint.bit_count(), JS/TS hand-roll the Kernighan loop. - C++ mutates
charcells directly; JS/TS/Python boards hold one-character strings, so digits must be converted withString(d)/str(d). - C++ wraps the state in a class; the other three use closures over local arrays, which is the idiomatic substitute.
- JS/TS bitwise operators truncate to 32 bits — irrelevant for 10-bit masks but worth knowing before widening the pattern.
Complexity
m = number of empty cells (≤ 81). The bound is never approached in practice; with MRV, typical puzzles take well under 10^4 placements.
Compare growth rates in the Complexity Explorer →When to use — and when not to
- Small fixed grids with local "once per group" constraints and a guaranteed solution.
- Any exact-cover puzzle where implementing Knuth's Algorithm X would be overkill.
- Large generalized Sudoku (
n² × n²forn ≥ 5) — plain backtracking blows up; use constraint propagation (naked singles, hidden singles) or Dancing Links. - You need to count all solutions of a nearly-empty board — the count is astronomically large.
Alternatives
Common mistakes
- Wrong box index —
(r // 3) * 3 + c // 3, notr // 3 + c // 3. - Not undoing the occupancy tables when a digit fails, corrupting later checks.
- Returning after the first digit placement instead of propagating the recursive result (
if solve(): return true). - Recomputing the row/col/box scan for every check (
O(27)instead ofO(1)) — fine for correctness but slow on hard boards.
Interview patterns
- Valid Sudoku (checking only) is the same three-table pass without recursion.
- Ask about MRV / forward checking when the interviewer says "the board is hard" — it shows you know why naive order can explode.
- Bitmask tables (
intper row/col/box) are the standard speed-up;popcountgives the candidate count for MRV.
- Recursion versus iterationIntermediate
- Recognizing a dynamic-programming problemAdvanced
- Word SearchAdvanced