diff --git a/README.md b/README.md index fb0f13e..34ee5a8 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,9 @@ cargo run -- sudoku-examples/hard.sudoku cargo run -- sudoku-examples/extreme.sudoku ``` +A visual explanation of the solver is available in +[`docs/solver-visual-guide.pdf`](docs/solver-visual-guide.pdf). + ## Project Decomposition - Command-line interface: `src/main.rs` diff --git a/docs/solver-visual-guide.html b/docs/solver-visual-guide.html new file mode 100644 index 0000000..6b6cbfe --- /dev/null +++ b/docs/solver-visual-guide.html @@ -0,0 +1,774 @@ + + + + + sudoku-ai solver visual guide + + + +
+

sudoku-ai visual guide

+
+
+

How the solver turns blanks into certainty

+

The engine is a compact constraint solver: it models every empty cell as a bit mask, repeatedly applies deterministic Sudoku deductions, then searches only when deduction stalls.

+
+ plain text input + row / column / block units + u128 candidates + naked singles + hidden singles + guided backtracking +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1348 + 7683 + 82174 + 54968 + 91582 + 835 + 3596871 + 64 + 172 + + + 2 4 6 + 7 + + + +
+ +
+
ParseRead whitespace-separated rows and validate the shape.
+
ModelPrecompute units and peers for each cell.
+
MaskTrack every candidate set as bits in a u128.
+
DeducePropagate placements, naked singles, hidden singles.
+
BranchPick the tightest unsolved cell and order values by impact.
+
SolveReturn the first complete state that survives constraints.
+
+
01 / overview
+
+ +
+

1. Input becomes a constraint model

+

The grid is square, but the solver thinks in units and peers

+

After parsing, each cell knows the three units it belongs to: one row, one column, and one block. The union of those units, excluding the cell itself, becomes its peer list.

+ +
+
+
0 2 3 0 +0 0 0 1 +1 0 0 0 +0 4 2 0
+

Blank cells are written as 0. A 4x4 puzzle uses 2x2 blocks; a 9x9 puzzle uses 3x3 blocks; the same machinery handles both.

+
+
3unit kinds per cell
+
27units in a 9x9 puzzle
+
20peers for a 9x9 cell
+
1state object to search
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + cell + + + + + row peers + same horizontal unit + + column peers + same vertical unit + + block peers + same sub-square + + selected cell + + + +

Peers are precomputed once, so assigning a value later is just a quick walk over affected cells.

+
+
+
02 / model
+
+ +
+

2. Candidates are bits, not lists

+

A cell’s possible values fit into one u128

+

Each value maps to one bit. Eliminating a candidate clears its bit; placing a value replaces the whole mask with exactly that bit.

+ +
+
+ + + + 9x9 full mask + values 1 through 9 are initially possible + + + 123 + 456 + 789 + + + + + + + + + + 11111 + 1111 + + + + + After peers place 1, 5, 8 + + + + + + + + + + 111111 + + + 000 + + + + + + mask & !value_bit(value) + + + +
+ +
+
+

Why masks work well here

+

Bit masks make candidate checks tiny: membership is mask & bit != 0, removal is mask &= !bit, and a naked single is count_ones() == 1.

+
+
+
+

Full mask

+

(1 << size) - 1 turns on every legal value bit.

+
+
+

Placed value

+

Once a cell is assigned, its candidate mask becomes exactly value_bit(value).

+
+
+

Contradiction

+

If an unsolved cell’s mask becomes zero, that path is rejected.

+
+
+
+
+
03 / candidates
+
+ +
+

3. Deduction runs until it stalls

+

Every assignment immediately removes pressure from the board

+

The solver loops over simple, strong rules. If any rule places a value, it starts another pass because that placement may unlock more forced moves.

+ +
+ + + + + + + + + + + + assign value + set value and exact mask + + + + propagate + clear this value from peers + + + + naked singles + one remaining candidate + + + + hidden singles + one place in a unit + + + repeat + until stable + + + +
+
+

Naked single

+

A cell with one remaining candidate is forced. The solver detects this with count_ones() == 1, assigns it, and propagates again.

+
+
+

Hidden single

+

For each row, column, and block, the solver checks every value. If only one unsolved cell in that unit can still hold the value, that cell is forced.

+
+
If a unit has no legal place for a value, or a candidate mask goes empty, that path is rejected as a contradiction.
+
+
+
04 / deduction
+
+ +
+

4. Search begins only after deduction stalls

+

The branch point is chosen to make guessing as constrained as possible

+

When no rule can place another value, the solver picks one unresolved cell and tries each candidate in a cloned state.

+ +
+
+ + + + Choose branch cell + Fewest candidates wins; ties prefer more unsolved peers. + + + {2,7} + 2 candidates + 13 open peers + + {1,5,9} + 3 candidates + 18 open peers + + {4,6} + 2 candidates + 17 open peers + selected + + + Order selected candidates + Impact = peers that would lose this value as a candidate. + + + + try 6 first + 17 + + + + + then 4 + 12 + + + + +
+ +
+
+

Cell heuristic

+

The solver scans unresolved cells and keeps the cell with the fewest candidate bits. If two cells are equally tight, it picks the one touching more unsolved peers.

+
+
+

Candidate heuristic

+

For that cell, values are sorted by impact. A value has higher impact when more peer cells currently contain that same value as a candidate.

+
+
+

Tie break

+

If candidate impacts are equal, the smaller value is tried first for deterministic output.

+
+
+
+
05 / search
+
+ +
+

5. Backtracking is just recursive state cloning

+

Every assumption either solves the board or collapses quickly

+

Search clones the current state, assigns one candidate, and immediately re-enters deduction. Failed branches vanish; successful branches return the solved grid.

+ +
+ + + + + deduced state + not solved, no forced moves + + + + + + try value 6 + contradiction during deduction + + + try value 4 + deduction continues + + + + + + more branching + same rules, deeper tree + + + complete grid + return solution + + + + + + + + +
+
+

The core rhythm

+
    +
  • Run deduction before every search decision.
  • +
  • If all cells are filled, return the state.
  • +
  • Otherwise choose one branch cell.
  • +
  • Try candidates in impact order.
  • +
  • Discard any branch that hits a contradiction.
  • +
+
+
Key idea
Most of the “intelligence” is in shrinking the search tree before guessing and making each guess remove as much uncertainty as possible.
+
+
+
06 / backtracking
+
+ +
+

Implementation map

+

Where to look in the code

+

The solver is small enough that the visual model maps directly onto a few functions in src/lib.rs.

+ +
+
+

Parsing

+

Puzzle::parse validates square dimensions, block layout, and value ranges before solving starts.

+
+
+

Constraint model

+

Solver::new creates row, column, and block units; build_peers turns them into peer lists.

+
+
+

Candidate masks

+

full_mask and value_bit encode possible values into a single integer.

+
+
+

Propagation

+

assign places a value, checks duplicate peers, and clears that value from every unsolved peer.

+
+
+

Deduction

+

deduce repeatedly applies naked singles and hidden singles until no rule progresses.

+
+
+

Search

+

search clones states, tries ordered candidates, and returns the first branch that reaches a complete solution.

+
+
+ +
+
Correctness guardContradictions stop invalid givens and impossible branches early.
+
Performance trickPeer lists and bit masks make the hot path small.
+
Search controlFewest candidates narrows the branch factor.
+
Impact orderingHigher-impact values test stronger assumptions first.
+
+
07 / code map
+
+ + diff --git a/docs/solver-visual-guide.pdf b/docs/solver-visual-guide.pdf new file mode 100644 index 0000000..0bd2385 Binary files /dev/null and b/docs/solver-visual-guide.pdf differ