Sudoku Solver
Initialize constraint sets and empty cells list
Scan the board to fill sets for rows, columns, and boxes with existing digits, and collect coordinates of empty cells.
rows = [set() for _ in range(9)]
cols = [set() for _ in range(9)]
boxes = [set() for _ in range(9)]
empties = []Identify first empty cell and compute candidates
Sort empty cells by number of valid candidates and select the most constrained cell to fill next.
empties.sort(key=lambda x: len(get_candidates(x[0], x[1])))
r, c = empties.pop(0)Try candidate '1' in cell (0,2)
Place digit '1' in cell (0,2), update constraint sets, and recurse deeper.
board[r][c] = ch
rows[r].add(ch)
cols[c].add(ch)
boxes[b].add(ch)Recurse to next empty cell after placing '1'
After placing '1', sort remaining empties and pick the next most constrained cell to fill.
empties.sort(key=lambda x: len(get_candidates(x[0], x[1])))
r, c = empties.pop(0)Try candidate '1' in cell (0,3) - conflict detected
Attempt to place '1' in cell (0,3), but '1' already exists in column 3, so this candidate is invalid.
for ch in get_candidates(r, c):Try candidate '2' in cell (0,3)
Place digit '2' in cell (0,3), update constraints, and recurse deeper.
board[r][c] = ch
rows[r].add(ch)
cols[c].add(ch)
boxes[b].add(ch)Backtrack after no candidates fit further down
Deeper recursion fails to find valid candidates for next cells, so algorithm backtracks by removing '2' from (0,3).
board[r][c] = '.'
rows[r].remove(ch)
cols[c].remove(ch)
boxes[b].remove(ch)Try candidate '4' in cell (0,3)
Place digit '4' in cell (0,3), update constraints, and recurse deeper.
board[r][c] = ch
rows[r].add(ch)
cols[c].add(ch)
boxes[b].add(ch)Continue recursion until all empties filled
The algorithm continues recursively filling cells with valid candidates, backtracking as needed, progressing deeper.
if backtrack():
return TrueAll empty cells filled - solution found
No empty cells remain, so the algorithm returns True, signaling a complete valid solution has been found.
if not empties:
return Truedef solveSudoku(board):
rows = [set() for _ in range(9)] # STEP 1
cols = [set() for _ in range(9)] # STEP 1
boxes = [set() for _ in range(9)] # STEP 1
empties = [] # STEP 1
for i in range(9): # STEP 1
for j in range(9): # STEP 1
if board[i][j] == '.':
empties.append((i,j))
else:
val = board[i][j]
rows[i].add(val)
cols[j].add(val)
boxes[(i//3)*3 + j//3].add(val)
def get_candidates(r, c):
b = (r//3)*3 + c//3
return [ch for ch in '123456789' if ch not in rows[r] and ch not in cols[c] and ch not in boxes[b]]
def backtrack():
if not empties: # STEP 10
return True
empties.sort(key=lambda x: len(get_candidates(x[0], x[1]))) # STEP 2,4,9
r, c = empties.pop(0) # STEP 2,4,9
b = (r//3)*3 + c//3
for ch in get_candidates(r, c): # STEP 3,5,6,8
board[r][c] = ch
rows[r].add(ch)
cols[c].add(ch)
boxes[b].add(ch)
if backtrack(): # STEP 9
return True
board[r][c] = '.' # STEP 7
rows[r].remove(ch)
cols[c].remove(ch)
boxes[b].remove(ch)
empties.insert(0, (r, c))
return False
backtrack() # STEP 1Key Takeaways
This insight is difficult to see from code alone because the sorting and candidate counting happen implicitly and recursively.
Watching the recursion tree reveals the depth-first search nature and how backtracking prunes invalid paths early.
This optimization is subtle in code but critical for performance and is clearly visible in the visualization as pruning decisions.
