1063-sudoku-solver — Sudoku Solver
Tutorial
The Problem
Sudoku is a constraint satisfaction problem: fill a 9×9 grid with digits 1–9 such that each row, column, and 3×3 box contains each digit exactly once. Backtracking with constraint checking is the standard algorithm — try each valid digit at each empty cell, backtrack when no digit works.
Sudoku solvers appear in puzzle generation systems, benchmarks for SAT solvers, and as interview problems testing backtracking proficiency. Peter Norvig's famous Python solver showed that constraint propagation (reducing possibilities without backtracking) handles most real puzzles without any backtracking.
🎯 Learning Outcomes
is_valid predicate that checks all three constraint typesCode Example
#![allow(clippy::all)]
// 1063: Sudoku Solver — Backtracking + Constraints
// Approach 1: Simple backtracking
fn solve_sudoku(board: &mut [[u8; 9]; 9]) -> bool {
fn is_valid(board: &[[u8; 9]; 9], row: usize, col: usize, num: u8) -> bool {
for i in 0..9 {
if board[row][i] == num || board[i][col] == num {
return false;
}
}
let (br, bc) = ((row / 3) * 3, (col / 3) * 3);
for r in br..br + 3 {
for c in bc..bc + 3 {
if board[r][c] == num {
return false;
}
}
}
true
}
fn solve(board: &mut [[u8; 9]; 9]) -> bool {
for r in 0..9 {
for c in 0..9 {
if board[r][c] == 0 {
for num in 1..=9 {
if is_valid(board, r, c, num) {
board[r][c] = num;
if solve(board) {
return true;
}
board[r][c] = 0;
}
}
return false;
}
}
}
true
}
solve(board)
}
// Approach 2: With constraint arrays for O(1) validation
fn solve_sudoku_fast(board: &mut [[u8; 9]; 9]) -> bool {
let mut rows = [[false; 10]; 9];
let mut cols = [[false; 10]; 9];
let mut boxes = [[false; 10]; 9];
for r in 0..9 {
for c in 0..9 {
let v = board[r][c] as usize;
if v != 0 {
rows[r][v] = true;
cols[c][v] = true;
boxes[(r / 3) * 3 + c / 3][v] = true;
}
}
}
fn solve(
board: &mut [[u8; 9]; 9],
rows: &mut [[bool; 10]; 9],
cols: &mut [[bool; 10]; 9],
boxes: &mut [[bool; 10]; 9],
) -> bool {
for r in 0..9 {
for c in 0..9 {
if board[r][c] == 0 {
let b = (r / 3) * 3 + c / 3;
for num in 1..=9usize {
if !rows[r][num] && !cols[c][num] && !boxes[b][num] {
board[r][c] = num as u8;
rows[r][num] = true;
cols[c][num] = true;
boxes[b][num] = true;
if solve(board, rows, cols, boxes) {
return true;
}
board[r][c] = 0;
rows[r][num] = false;
cols[c][num] = false;
boxes[b][num] = false;
}
}
return false;
}
}
}
true
}
solve(board, &mut rows, &mut cols, &mut boxes)
}
#[cfg(test)]
mod tests {
use super::*;
fn test_board() -> [[u8; 9]; 9] {
[
[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],
]
}
#[test]
fn test_sudoku_simple() {
let mut board = test_board();
assert!(solve_sudoku(&mut board));
assert_eq!(board[0][2], 4);
assert_eq!(board[4][4], 5);
}
#[test]
fn test_sudoku_fast() {
let mut board = test_board();
assert!(solve_sudoku_fast(&mut board));
assert_eq!(board[0][2], 4);
assert_eq!(board[4][4], 5);
}
}Key Differences
&mut [[u8; 9]; 9]; OCaml uses int array array — both support direct index assignment.bool to signal success/failure; Rust's return true/false is explicit while OCaml uses the last expression.u16 (bit per digit) for O(1) constraint checks; the bitmask logic is identical.OCaml Approach
let solve board =
let is_valid r c num =
let rec check i =
if i = 9 then true
else if board.(r).(i) = num || board.(i).(c) = num then false
else check (i + 1)
in
let br, bc = (r/3)*3, (c/3)*3 in
check 0 && (* also check 3x3 box *)
let rec check_box i =
if i = 9 then true
else if board.(br + i/3).(bc + i mod 3) = num then false
else check_box (i + 1)
in check_box 0
in
(* ... backtracking solve *)
The logic is identical. OCaml's board.(r).(c) vs Rust's board[r][c] is the main syntactic difference.
Full Source
#![allow(clippy::all)]
// 1063: Sudoku Solver — Backtracking + Constraints
// Approach 1: Simple backtracking
fn solve_sudoku(board: &mut [[u8; 9]; 9]) -> bool {
fn is_valid(board: &[[u8; 9]; 9], row: usize, col: usize, num: u8) -> bool {
for i in 0..9 {
if board[row][i] == num || board[i][col] == num {
return false;
}
}
let (br, bc) = ((row / 3) * 3, (col / 3) * 3);
for r in br..br + 3 {
for c in bc..bc + 3 {
if board[r][c] == num {
return false;
}
}
}
true
}
fn solve(board: &mut [[u8; 9]; 9]) -> bool {
for r in 0..9 {
for c in 0..9 {
if board[r][c] == 0 {
for num in 1..=9 {
if is_valid(board, r, c, num) {
board[r][c] = num;
if solve(board) {
return true;
}
board[r][c] = 0;
}
}
return false;
}
}
}
true
}
solve(board)
}
// Approach 2: With constraint arrays for O(1) validation
fn solve_sudoku_fast(board: &mut [[u8; 9]; 9]) -> bool {
let mut rows = [[false; 10]; 9];
let mut cols = [[false; 10]; 9];
let mut boxes = [[false; 10]; 9];
for r in 0..9 {
for c in 0..9 {
let v = board[r][c] as usize;
if v != 0 {
rows[r][v] = true;
cols[c][v] = true;
boxes[(r / 3) * 3 + c / 3][v] = true;
}
}
}
fn solve(
board: &mut [[u8; 9]; 9],
rows: &mut [[bool; 10]; 9],
cols: &mut [[bool; 10]; 9],
boxes: &mut [[bool; 10]; 9],
) -> bool {
for r in 0..9 {
for c in 0..9 {
if board[r][c] == 0 {
let b = (r / 3) * 3 + c / 3;
for num in 1..=9usize {
if !rows[r][num] && !cols[c][num] && !boxes[b][num] {
board[r][c] = num as u8;
rows[r][num] = true;
cols[c][num] = true;
boxes[b][num] = true;
if solve(board, rows, cols, boxes) {
return true;
}
board[r][c] = 0;
rows[r][num] = false;
cols[c][num] = false;
boxes[b][num] = false;
}
}
return false;
}
}
}
true
}
solve(board, &mut rows, &mut cols, &mut boxes)
}
#[cfg(test)]
mod tests {
use super::*;
fn test_board() -> [[u8; 9]; 9] {
[
[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],
]
}
#[test]
fn test_sudoku_simple() {
let mut board = test_board();
assert!(solve_sudoku(&mut board));
assert_eq!(board[0][2], 4);
assert_eq!(board[4][4], 5);
}
#[test]
fn test_sudoku_fast() {
let mut board = test_board();
assert!(solve_sudoku_fast(&mut board));
assert_eq!(board[0][2], 4);
assert_eq!(board[4][4], 5);
}
}#[cfg(test)]
mod tests {
use super::*;
fn test_board() -> [[u8; 9]; 9] {
[
[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],
]
}
#[test]
fn test_sudoku_simple() {
let mut board = test_board();
assert!(solve_sudoku(&mut board));
assert_eq!(board[0][2], 4);
assert_eq!(board[4][4], 5);
}
#[test]
fn test_sudoku_fast() {
let mut board = test_board();
assert!(solve_sudoku_fast(&mut board));
assert_eq!(board[0][2], 4);
assert_eq!(board[4][4], 5);
}
}
Deep Comparison
Sudoku Solver — Comparison
Core Insight
Sudoku is backtracking with three overlapping constraints (row, column, 3×3 box). Maintaining constraint arrays reduces validation to O(1) per candidate. Both languages use in-place mutation with undo on backtrack.
OCaml Approach
ref for found-cell trackingfor loops with ref flags for early exit simulationArray.init 9 (fun _ -> Array.make 10 false) for constraint arrays(r/3)*3 + c/3Rust Approach
[[u8; 9]; 9] — stack-allocated, cache-friendlyfor loops with early return false — cleaner control flow[[bool; 10]; 9] constraint arrays — also stack-allocatedfn with explicit mutable referencesComparison Table
| Aspect | OCaml | Rust |
|---|---|---|
| Board type | int array array (heap) | [[u8; 9]; 9] (stack) |
| Constraint arrays | bool array array | [[bool; 10]; 9] (fixed) |
| Empty cell search | ref None + iteration | Nested for with early return |
| Backtrack | board.(r).(c) <- 0 | board[r][c] = 0 |
| Early exit | ref flag + not !solved check | return false / return true |
Exercises
row_mask: [u16; 9], col_mask: [u16; 9], box_mask: [u16; 9] to replace the O(9) validity checks with O(1) bit operations.