Lab: Sudoku CSP Solver
Unit 4: Optimization and Constraint Satisfaction — Sudoku CSP Solver Lab (125 points)
Lab Overview
In this lab, you will build a complete Sudoku solver from scratch using constraint satisfaction techniques.
Starting from an empty SudokuCSP class, you will implement the full pipeline: CSP formulation, basic backtracking, the MRV heuristic, and AC-3 arc consistency.
By combining all four components, your solver will handle Easy, Medium, and Hard puzzles efficiently.
Before starting, review:
-
Section 4.3 — CSPs: Variables, domains, constraints, Sudoku formulation
-
Section 4.4 — Backtracking Search: Backtracking algorithm, forward checking, AC-3
-
Section 4.5 — Smart Heuristics: MRV, degree heuristic, LCV
Learning Objectives
By completing this lab, you will be able to:
-
Represent Sudoku as a formal CSP with variables, domains, and constraints
-
Implement backtracking search for constraint satisfaction
-
Apply the Minimum Remaining Values (MRV) heuristic for smarter variable selection
-
Implement the AC-3 algorithm for enforcing arc consistency
-
Combine all techniques into an efficient, working Sudoku solver
Setup: Open the Notebook
Download the starter notebook:
Open in Google Colab:
-
Click File → Upload notebook
-
Upload the downloaded
.ipynbfile -
Begin coding in the TODO sections
Part 1: CSP Formulation (20 points)
The SudokuCSP class needs three foundational methods that establish the problem’s variables, domains, and constraints.
The Sudoku CSP
A 9×9 Sudoku puzzle is formulated as:
-
Variables: 81 cells, each indexed by (row, col) where row, col ∈ {0, …, 8}
-
Domains: {1, 2, 3, 4, 5, 6, 7, 8, 9} for empty cells; {given_digit} for pre-filled cells
-
Constraints: All-different for each row (9), each column (9), and each 3×3 box (9) — 27 constraints total
TODO #1: Initialize Domains (5 points)
def _initialize_domains(self):
"""
Set up initial domains for all cells.
For empty cells (board value == 0): domain = {1, 2, 3, 4, 5, 6, 7, 8, 9}
For filled cells (board value != 0): domain = {board_value}
Store domains in self.domains as a dict: {(row, col): set_of_values}
TODO: Implement using nested loops over range(self.size)
"""
pass
TODO #2: Get Neighbors (10 points)
def get_neighbors(self, cell):
"""
Return the set of all cells that share a constraint with `cell`.
Neighbors include all cells in:
- The same row
- The same column
- The same 3×3 box
Parameters:
cell (tuple): (row, col)
Returns:
set: All (row, col) tuples that constrain this cell (excluding cell itself)
TODO: Implement using three loops (row, column, box)
"""
pass
Finding the 3×3 box:
For a cell at (row, col), the top-left corner of its 3×3 box is at:
-
box_row = (row // 3) * 3 -
box_col = (col // 3) * 3
Then iterate i from box_row to box_row + 3, and j from box_col to box_col + 3.
Each cell has exactly 20 neighbors: 8 in the same row + 8 in the same column + 8 in the same box, minus the 4 cells counted twice (the intersection of row, column, and box).
TODO #3: Check Consistency (5 points)
def is_consistent(self, cell, value):
"""
Return True if assigning `value` to `cell` is consistent with all
currently assigned neighbors.
A value is consistent if no neighbor with a fixed domain of size 1
already has the same value.
Parameters:
cell (tuple): (row, col) to potentially assign
value (int): Value to check
Returns:
bool: True if consistent, False if conflict detected
TODO: Iterate over neighbors; check if any neighbor is assigned
the same value
"""
pass
Part 2: Basic Backtracking (25 points)
TODO #4: Select Unassigned Variable (Basic) (5 points)
def select_unassigned_variable_basic(self):
"""
Return the first unassigned cell (domain size > 1).
Scan cells in row-major order: (0,0), (0,1), ..., (8,8).
Returns:
tuple: (row, col) of first unassigned cell, or None if all assigned
TODO: Scan all cells and return the first with domain size > 1
"""
pass
TODO #5: Backtracking Search (20 points)
def backtracking_search(self):
"""
Main backtracking search entry point.
Uses select_unassigned_variable_basic() and is_consistent().
Returns:
bool: True if solution found, False if no solution exists
When a value is assigned:
- Set self.domains[cell] = {value}
- Recurse
- If recursion fails, restore the original domain (backtrack)
TODO: Implement recursive backtracking
"""
return self._backtrack_basic()
def _backtrack_basic(self):
"""Recursive backtracking helper."""
pass
Backtracking pattern:
-
Select an unassigned variable. If none, return True (complete assignment).
-
For each value
vin the variable’s domain:-
If
is_consistent(cell, v): assignself.domains[cell] = {v} -
Recurse; if successful, return True
-
Otherwise, restore the original domain (undo the assignment)
-
-
Return False if no value worked
Part 3: MRV Heuristic (30 points)
TODO #6: MRV Variable Selection (30 points)
def select_unassigned_variable_mrv(self):
"""
Return the unassigned cell with the fewest legal values (MRV heuristic).
Break ties by choosing the cell with the most neighbors that are
unassigned (degree heuristic).
Returns:
tuple: (row, col) of the most constrained unassigned cell,
or None if all cells are assigned.
TODO: Find the minimum domain size among unassigned cells,
then apply degree heuristic tiebreaking.
"""
pass
Degree heuristic tiebreaking:
When multiple cells tie for smallest domain, count how many of each cell’s neighbors are still unassigned (domain size > 1). Choose the cell with the highest count — it constrains more future assignments.
You can implement this in two passes:
1. Find min_size = minimum domain size among unassigned cells
2. Among all cells with domain_size == min_size, return the one with the most unassigned neighbors
Once select_unassigned_variable_mrv() is implemented, add a backtracking_search_mrv() method that is identical to your basic backtracking but calls select_unassigned_variable_mrv() instead of select_unassigned_variable_basic().
Part 4: AC-3 Algorithm (30 points)
TODO #7: REVISE Function (15 points)
def revise(self, xi, xj):
"""
Make arc (xi → xj) arc-consistent.
Remove from xi's domain any value that has no support in xj's domain.
A value `x` in xi's domain has "support" if there exists some `y` in xj's
domain such that x != y (the all-different constraint).
Parameters:
xi (tuple): (row, col) of the variable whose domain may be reduced
xj (tuple): (row, col) of the neighbor variable
Returns:
bool: True if any value was removed from xi's domain, False otherwise
TODO: For each value in xi's domain, check if it has any support in xj's
domain. Remove values with no support.
"""
pass
All-different constraint support:
For Sudoku’s all-different constraints, value x from xi has support in xj if xj’s domain contains at least one value y where x != y.
In other words: is there any value in xj’s domain that is different from x?
If xj’s domain = {5} and x = 5, then x has no support — remove x from xi’s domain.
If xj’s domain = {3, 7} and x = 5, then x has support (both 3 and 7 differ from 5) — keep x.
TODO #8: AC-3 Algorithm (15 points)
def ac3(self):
"""
Enforce arc consistency across the entire CSP using the AC-3 algorithm.
Initialize the queue with all arcs (xi, xj) for every constrained pair.
Process arcs: if REVISE reduces xi's domain, add all arcs (xk, xi) back.
Returns:
bool: True if arc-consistent (no empty domains), False if contradiction
TODO: Implement using a deque (collections.deque).
Initialize queue with all (xi, xj) pairs where xi and xj are neighbors.
"""
pass
Starting the queue:
from collections import deque
queue = deque()
for cell in all_cells:
for neighbor in self.get_neighbors(cell):
queue.append((cell, neighbor))
Then process: dequeue (xi, xj), call revise(xi, xj), and if revised, add all (xk, xi) back for each neighbor xk of xi (except xj).
Part 5: Complete Solver (20 points)
TODO #9: Integrated Solver (20 points)
def solve_sudoku(self):
"""
Solve the Sudoku puzzle using the full pipeline:
1. Run AC-3 as preprocessing (reduce domains before search)
2. If AC-3 returns False, the puzzle has no solution
3. If all domains have size 1 after AC-3, puzzle is solved
4. Otherwise, run backtracking with MRV heuristic
Returns:
bool: True if solved successfully, False if no solution exists
TODO: Implement the two-phase solver
"""
pass
Grading Rubric
| Component | Function | Points |
|---|---|---|
CSP Formulation |
|
5 |
CSP Formulation |
|
10 |
CSP Formulation |
|
5 |
Backtracking |
|
5 |
Backtracking |
|
20 |
MRV Heuristic |
|
30 |
AC-3 |
|
15 |
AC-3 |
|
15 |
Complete Solver |
|
20 |
Total |
125 |
Submission Instructions
Submitting Your Lab:
-
Complete all TODO sections in the notebook
-
Test your solver on all three puzzles (Easy, Medium, Hard)
-
Verify solutions with
validate_solution()— all three should returnTrue -
Ensure timing meets the performance targets in the table above
-
Download: File → Download → Download .ipynb
-
Submit to Brightspace under Week 4: Sudoku CSP Solver Lab
Optional: Review the complete implementation with detailed explanations in the CSP Solver Walkthrough.
Lab code adapted from aima-python, MIT License, Copyright 2016 aima-python contributors.
This work is licensed under CC BY-SA 4.0.