Master Sudoku solving with backtracking — the rules of Sudoku, basic backtracking solver, O(1) validity checking with sets, naked singles optimization, constraint propagation, forward checking, and how to build an efficient solver that handles even hard Sudoku puzzles.
Sudoku Rules
A 9×9 grid divided into nine 3×3 boxes. Fill in digits 1-9 such that:
- Each row contains digits 1-9, each exactly once
- Each column contains digits 1-9, each exactly once
- Each 3×3 box contains digits 1-9, each exactly once
Input (0 = empty cell):
5 3 0 | 0 7 0 | 0 0 0
6 0 0 | 1 9 5 | 0 0 0
0 9 8 | 0 0 0 | 0 6 0
------+-------+------
8 0 0 | 0 6 0 | 0 0 3
4 0 0 | 8 0 3 | 0 0 1
7 0 0 | 0 2 0 | 0 0 6
------+-------+------
0 6 0 | 0 0 0 | 2 8 0
0 0 0 | 4 1 9 | 0 0 5
0 0 0 | 0 8 0 | 0 7 9
Solution 2 — Optimized with Sets: O(1) Validity Check
def solve_sudoku_fast(board):
"""
Optimized: maintain sets for rows, columns, and boxes.
Validity check becomes O(1) instead of O(9).
"""
# Initialize constraint sets from existing numbers
rows = [set() for _ in range(9)]
cols = [set() for _ in range(9)]
boxes = [set() for _ in range(9)] # Box index = (row//3)*3 + col//3
empty_cells = []
for r in range(9):
for c in range(9):
val = board[r][c]
box_idx = (r // 3) * 3 + (c // 3)
if val != 0:
rows[r].add(val)
cols[c].add(val)
boxes[box_idx].add(val)
else:
empty_cells.append((r, c))
def is_valid(r, c, num):
"""O(1) validity check using sets."""
box_idx = (r // 3) * 3 + (c // 3)
return num not in rows[r] and num not in cols[c] and num not in boxes[box_idx]
def backtrack(idx):
if idx == len(empty_cells):
return True # All cells filled
r, c = empty_cells[idx]
box_idx = (r // 3) * 3 + (c // 3)
for num in range(1, 10):
if is_valid(r, c, num):
# CHOOSE: Place num
board[r][c] = num
rows[r].add(num)
cols[c].add(num)
boxes[box_idx].add(num)
# EXPLORE
if backtrack(idx + 1):
return True
# UNDO: Remove num
board[r][c] = 0
rows[r].remove(num)
cols[c].remove(num)
boxes[box_idx].remove(num)
return False # No digit works → backtrack
return backtrack(0)
Solution 3 — Minimum Remaining Values (MRV) Heuristic
Idea: Instead of trying cells in order (left to right, top to bottom), always try the cell with the fewest valid options first. This dramatically reduces backtracking.
def solve_sudoku_mrv(board):
"""
MRV (Minimum Remaining Values) heuristic:
Always fill the cell with fewest valid choices first.
This often solves hard Sudoku without any backtracking.
"""
rows = [set() for _ in range(9)]
cols = [set() for _ in range(9)]
boxes = [set() for _ in range(9)]
for r in range(9):
for c in range(9):
val = board[r][c]
if val != 0:
box = (r//3)*3 + c//3
rows[r].add(val); cols[c].add(val); boxes[box].add(val)
def get_candidates(r, c):
"""Get set of valid digits for cell (r, c)."""
box = (r//3)*3 + c//3
used = rows[r] | cols[c] | boxes[box]
return set(range(1, 10)) - used
def find_best_cell():
"""Find empty cell with minimum candidates (MRV)."""
best_r, best_c, best_cands = None, None, set(range(1, 11))
for r in range(9):
for c in range(9):
if board[r][c] == 0:
cands = get_candidates(r, c)
if len(cands) < len(best_cands):
best_r, best_c, best_cands = r, c, cands
return best_r, best_c, best_cands
def backtrack():
r, c, candidates = find_best_cell()
if r is None:
return True # No empty cells left — solved!
if not candidates:
return False # Cell has no valid options — backtrack
box = (r//3)*3 + c//3
for num in candidates:
board[r][c] = num
rows[r].add(num); cols[c].add(num); boxes[box].add(num)
if backtrack():
return True
board[r][c] = 0
rows[r].remove(num); cols[c].remove(num); boxes[box].remove(num)
return False
return backtrack()
Constraint Propagation — Naked Singles
Naked singles: If a cell has only one valid candidate, it MUST be that number. Propagate this constraint before backtracking.
def naked_singles_propagation(board, rows, cols, boxes):
"""
Repeatedly fill cells with only one valid option.
Returns False if a contradiction is found.
Returns number of cells filled.
"""
filled = 0
changed = True
while changed:
changed = False
for r in range(9):
for c in range(9):
if board[r][c] == 0:
box = (r//3)*3 + c//3
candidates = set(range(1,10)) - rows[r] - cols[c] - boxes[box]
if len(candidates) == 0:
return -1 # Contradiction — no valid options!
if len(candidates) == 1:
num = candidates.pop()
board[r][c] = num
rows[r].add(num); cols[c].add(num); boxes[box].add(num)
filled += 1
changed = True
return filled
Complete Solver with All Optimizations
def solve_sudoku_complete(board):
"""
Full-featured solver: naked singles + MRV + backtracking.
Can solve any valid 9×9 Sudoku.
"""
rows = [set() for _ in range(9)]
cols = [set() for _ in range(9)]
boxes = [set() for _ in range(9)]
for r in range(9):
for c in range(9):
val = board[r][c]
if val != 0:
box = (r//3)*3 + c//3
rows[r].add(val); cols[c].add(val); boxes[box].add(val)
def get_candidates(r, c):
box = (r//3)*3 + c//3
return set(range(1,10)) - rows[r] - cols[c] - boxes[box]
def find_mrv_cell():
best = None
for r in range(9):
for c in range(9):
if board[r][c] == 0:
cands = get_candidates(r, c)
if not cands: return r, c, set() # Contradiction
if best is None or len(cands) < len(best[2]):
best = (r, c, cands)
return best if best else (None, None, None)
def backtrack():
r, c, cands = find_mrv_cell()
if r is None: return True # Solved
if not cands: return False # Contradiction
box = (r//3)*3 + c//3
for num in sorted(cands): # Try smallest first
board[r][c] = num
rows[r].add(num); cols[c].add(num); boxes[box].add(num)
if backtrack(): return True
board[r][c] = 0
rows[r].discard(num); cols[c].discard(num); boxes[box].discard(num)
return False
return backtrack()
Testing
def print_board(board):
for r in range(9):
if r % 3 == 0 and r != 0:
print("-" * 21)
row_str = ""
for c in range(9):
if c % 3 == 0 and c != 0:
row_str += "| "
row_str += str(board[r][c]) + " "
print(row_str)
# Famous "hardest Sudoku" — Al Escargot
board = [
[1,0,0,0,0,7,0,9,0],
[0,3,0,0,2,0,0,0,8],
[0,0,9,6,0,0,5,0,0],
[0,0,5,3,0,0,9,0,0],
[0,1,0,0,8,0,0,0,2],
[6,0,0,0,0,4,0,0,0],
[3,0,0,0,0,0,0,1,0],
[0,4,0,0,0,0,0,0,7],
[0,0,7,0,0,0,3,0,0],
]
import copy
b = copy.deepcopy(board)
print("Solving...")
solve_sudoku_complete(b)
print_board(b)
Complexity Analysis
| Approach | Time (worst) | Space | Performance |
|---|
| Basic backtracking | O(9^81) | O(1) | Slow on hard puzzles |
| With sets (O(1) check) | O(9^81) | O(81) | Faster in practice |
| MRV heuristic | O(9^empty_cells) | O(81) | Much faster |
| + Naked singles | Typically O(n²) | O(81) | Near-instant for most |
In practice, combining naked singles propagation with MRV solves 99% of published Sudoku puzzles without any backtracking at all — backtracking is only needed for "evil" difficulty puzzles.
Summary
Sudoku solving demonstrates how backtracking combined with constraint propagation can turn an O(9^81) worst case into a near-instant solution for real inputs:
- Basic backtracking: Try each digit, validate, backtrack if invalid
- Set-based validation: O(1) validity check with row/col/box sets
- MRV heuristic: Fill most constrained cells first to reduce branching
- Naked singles: Propagate cells with only one option before backtracking
- Combined: Near-instant solution for virtually all real Sudoku puzzles
*Next Lesson: Subset Generation*