docs: add solver visual guide
Add a seven-page visual PDF that explains how the Sudoku solver parses input, builds row/column/block constraints, stores candidates as bit masks, applies naked and hidden singles, and orders recursive search branches. The guide is authored as plain HTML/CSS/SVG so the diagrams stay reviewable and can be regenerated without a separate design tool. The README now links to the PDF artifact for quick discovery. Test Plan: - cargo test - pdfinfo docs/solver-visual-guide.pdf - Raster-inspected exported PDF pages with pdftoppm Refs: none
This commit is contained in:
@@ -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`
|
||||
|
||||
@@ -0,0 +1,774 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>sudoku-ai solver visual guide</title>
|
||||
<style>
|
||||
@page {
|
||||
size: A4 landscape;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
color: #152033;
|
||||
background: #f5f0e8;
|
||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
.page {
|
||||
position: relative;
|
||||
width: 297mm;
|
||||
height: 210mm;
|
||||
overflow: hidden;
|
||||
page-break-after: always;
|
||||
background:
|
||||
linear-gradient(135deg, rgba(255, 255, 255, 0.94), rgba(244, 239, 230, 0.94)),
|
||||
radial-gradient(circle at 12% 14%, rgba(255, 183, 67, 0.22), transparent 30%),
|
||||
radial-gradient(circle at 90% 86%, rgba(27, 158, 142, 0.18), transparent 35%);
|
||||
padding: 18mm 20mm;
|
||||
}
|
||||
|
||||
.page:last-child {
|
||||
page-break-after: auto;
|
||||
}
|
||||
|
||||
.kicker {
|
||||
margin: 0 0 5mm;
|
||||
color: #0c7f78;
|
||||
font-size: 10pt;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
margin: 0;
|
||||
color: #101827;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
max-width: 180mm;
|
||||
font-size: 40pt;
|
||||
line-height: 1.02;
|
||||
}
|
||||
|
||||
h2 {
|
||||
max-width: 200mm;
|
||||
font-size: 27pt;
|
||||
line-height: 1.08;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: #405064;
|
||||
font-size: 12pt;
|
||||
line-height: 1.42;
|
||||
}
|
||||
|
||||
.lead {
|
||||
max-width: 165mm;
|
||||
margin-top: 8mm;
|
||||
color: #334357;
|
||||
font-size: 15pt;
|
||||
line-height: 1.42;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
|
||||
font-size: 0.92em;
|
||||
}
|
||||
|
||||
.title-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 104mm;
|
||||
gap: 17mm;
|
||||
align-items: center;
|
||||
height: 116mm;
|
||||
margin-top: 8mm;
|
||||
}
|
||||
|
||||
.badges {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 3mm;
|
||||
margin-top: 11mm;
|
||||
}
|
||||
|
||||
.badge {
|
||||
padding: 2.2mm 3.5mm;
|
||||
border: 0.45mm solid rgba(16, 24, 39, 0.11);
|
||||
border-radius: 99px;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
color: #203146;
|
||||
font-size: 10pt;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.flow {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
gap: 4mm;
|
||||
margin-top: 14mm;
|
||||
}
|
||||
|
||||
.flow-step,
|
||||
.card,
|
||||
.stat,
|
||||
.legend-item {
|
||||
border: 0.45mm solid rgba(16, 24, 39, 0.11);
|
||||
border-radius: 2mm;
|
||||
background: rgba(255, 255, 255, 0.78);
|
||||
box-shadow: 0 1.5mm 6mm rgba(20, 29, 42, 0.08);
|
||||
}
|
||||
|
||||
.flow-step {
|
||||
position: relative;
|
||||
min-height: 26mm;
|
||||
padding: 4mm;
|
||||
}
|
||||
|
||||
.flow-step:not(:last-child)::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 11mm;
|
||||
right: -4.6mm;
|
||||
width: 5.2mm;
|
||||
height: 5.2mm;
|
||||
border-top: 1mm solid #df6e49;
|
||||
border-right: 1mm solid #df6e49;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.flow-step strong {
|
||||
display: block;
|
||||
margin-bottom: 1.8mm;
|
||||
color: #111827;
|
||||
font-size: 10.5pt;
|
||||
}
|
||||
|
||||
.flow-step span {
|
||||
color: #536477;
|
||||
font-size: 8.5pt;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.two-col {
|
||||
display: grid;
|
||||
grid-template-columns: 118mm 1fr;
|
||||
gap: 12mm;
|
||||
align-items: start;
|
||||
margin-top: 12mm;
|
||||
}
|
||||
|
||||
.three-col {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8mm;
|
||||
margin-top: 12mm;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 6mm;
|
||||
}
|
||||
|
||||
.card h3 {
|
||||
margin: 0 0 3mm;
|
||||
color: #102030;
|
||||
font-size: 15pt;
|
||||
line-height: 1.12;
|
||||
}
|
||||
|
||||
.card p,
|
||||
.card li {
|
||||
color: #4b5c70;
|
||||
font-size: 10.4pt;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 3mm 0 0;
|
||||
padding-left: 5mm;
|
||||
}
|
||||
|
||||
li {
|
||||
margin: 1.8mm 0;
|
||||
}
|
||||
|
||||
.code {
|
||||
white-space: pre;
|
||||
border-radius: 2mm;
|
||||
background: #101827;
|
||||
color: #e7f2f0;
|
||||
padding: 5mm;
|
||||
font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
|
||||
font-size: 9.8pt;
|
||||
line-height: 1.42;
|
||||
box-shadow: 0 1.5mm 6mm rgba(16, 24, 39, 0.16);
|
||||
}
|
||||
|
||||
.svg-frame {
|
||||
border: 0.45mm solid rgba(16, 24, 39, 0.10);
|
||||
border-radius: 2mm;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
box-shadow: 0 1.5mm 6mm rgba(20, 29, 42, 0.08);
|
||||
}
|
||||
|
||||
.caption {
|
||||
margin-top: 3mm;
|
||||
color: #627084;
|
||||
font-size: 9.2pt;
|
||||
line-height: 1.32;
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 5mm;
|
||||
margin-top: 10mm;
|
||||
}
|
||||
|
||||
.stat {
|
||||
padding: 4.5mm;
|
||||
}
|
||||
|
||||
.stat b {
|
||||
display: block;
|
||||
color: #132133;
|
||||
font-size: 20pt;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.stat span {
|
||||
color: #5d6c7d;
|
||||
font-size: 9.3pt;
|
||||
}
|
||||
|
||||
.pill {
|
||||
display: inline-block;
|
||||
padding: 1.4mm 2.5mm;
|
||||
border-radius: 99px;
|
||||
color: #102030;
|
||||
background: #ffcf66;
|
||||
font-size: 9pt;
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.page-number {
|
||||
position: absolute;
|
||||
right: 14mm;
|
||||
bottom: 4mm;
|
||||
color: #8994a2;
|
||||
font-size: 8pt;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.dark {
|
||||
color: #eaf3f2;
|
||||
background:
|
||||
linear-gradient(135deg, #101827 0%, #163343 58%, #0c7f78 100%);
|
||||
}
|
||||
|
||||
.dark h1,
|
||||
.dark h2,
|
||||
.dark .kicker {
|
||||
color: #fffaf0;
|
||||
}
|
||||
|
||||
.dark p,
|
||||
.dark .lead {
|
||||
color: #d4e1df;
|
||||
}
|
||||
|
||||
.dark .badge {
|
||||
border-color: rgba(255, 255, 255, 0.14);
|
||||
background: rgba(255, 255, 255, 0.10);
|
||||
color: #f4fbfa;
|
||||
}
|
||||
|
||||
.dark .page-number {
|
||||
color: rgba(255, 255, 255, 0.58);
|
||||
}
|
||||
|
||||
.dark .card h3 {
|
||||
color: #101827;
|
||||
}
|
||||
|
||||
.dark .card p,
|
||||
.dark .card li {
|
||||
color: #4b5c70;
|
||||
}
|
||||
|
||||
.dark .flow-step strong {
|
||||
color: #101827;
|
||||
}
|
||||
|
||||
.dark .flow-step span {
|
||||
color: #536477;
|
||||
}
|
||||
|
||||
.callout {
|
||||
margin-top: 9mm;
|
||||
padding: 5mm 6mm;
|
||||
border-left: 2mm solid #df6e49;
|
||||
border-radius: 0 2mm 2mm 0;
|
||||
background: rgba(255, 255, 255, 0.76);
|
||||
color: #35465a;
|
||||
font-size: 12pt;
|
||||
line-height: 1.4;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<section class="page dark">
|
||||
<p class="kicker">sudoku-ai visual guide</p>
|
||||
<div class="title-grid">
|
||||
<div>
|
||||
<h1>How the solver turns blanks into certainty</h1>
|
||||
<p class="lead">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.</p>
|
||||
<div class="badges">
|
||||
<span class="badge">plain text input</span>
|
||||
<span class="badge">row / column / block units</span>
|
||||
<span class="badge"><span class="mono">u128</span> candidates</span>
|
||||
<span class="badge">naked singles</span>
|
||||
<span class="badge">hidden singles</span>
|
||||
<span class="badge">guided backtracking</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<svg width="390" height="390" viewBox="0 0 390 390" aria-label="Sudoku grid with solver highlights">
|
||||
<defs>
|
||||
<linearGradient id="cellGlow" x1="0" x2="1" y1="0" y2="1">
|
||||
<stop stop-color="#ffcf66"/>
|
||||
<stop offset="1" stop-color="#df6e49"/>
|
||||
</linearGradient>
|
||||
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feDropShadow dx="0" dy="8" stdDeviation="10" flood-color="#06101a" flood-opacity="0.32"/>
|
||||
</filter>
|
||||
</defs>
|
||||
<rect x="17" y="17" width="356" height="356" rx="14" fill="#fdfbf7" filter="url(#shadow)"/>
|
||||
<g transform="translate(31 31)">
|
||||
<rect width="328" height="328" rx="8" fill="#fffaf0"/>
|
||||
<g fill="#f1f5f4">
|
||||
<rect x="0" y="0" width="109.3" height="109.3"/>
|
||||
<rect x="218.7" y="0" width="109.3" height="109.3"/>
|
||||
<rect x="109.3" y="109.3" width="109.3" height="109.3"/>
|
||||
<rect x="0" y="218.7" width="109.3" height="109.3"/>
|
||||
<rect x="218.7" y="218.7" width="109.3" height="109.3"/>
|
||||
</g>
|
||||
<rect x="145.8" y="145.8" width="36.4" height="36.4" fill="url(#cellGlow)" opacity="0.92"/>
|
||||
<g stroke="#cad3d6" stroke-width="1">
|
||||
<path d="M36.4 0V328M72.9 0V328M145.8 0V328M182.2 0V328M255.1 0V328M291.6 0V328"/>
|
||||
<path d="M0 36.4H328M0 72.9H328M0 145.8H328M0 182.2H328M0 255.1H328M0 291.6H328"/>
|
||||
</g>
|
||||
<g stroke="#152033" stroke-width="4">
|
||||
<path d="M109.3 0V328M218.7 0V328M0 109.3H328M0 218.7H328"/>
|
||||
<rect width="328" height="328" fill="none"/>
|
||||
</g>
|
||||
<g fill="#152033" font-size="24" font-weight="850" text-anchor="middle" dominant-baseline="middle">
|
||||
<text x="18.2" y="18.2">1</text><text x="163.9" y="18.2">3</text><text x="200.4" y="18.2">4</text><text x="309.8" y="18.2">8</text>
|
||||
<text x="54.7" y="54.7">7</text><text x="127.5" y="54.7">6</text><text x="163.9" y="54.7">8</text><text x="273.3" y="54.7">3</text>
|
||||
<text x="91.1" y="91.1">8</text><text x="127.5" y="91.1">2</text><text x="163.9" y="91.1">1</text><text x="236.9" y="91.1">7</text><text x="309.8" y="91.1">4</text>
|
||||
<text x="54.7" y="127.5">5</text><text x="91.1" y="127.5">4</text><text x="163.9" y="127.5">9</text><text x="236.9" y="127.5">6</text><text x="273.3" y="127.5">8</text>
|
||||
<text x="18.2" y="163.9">9</text><text x="54.7" y="163.9">1</text><text x="127.5" y="163.9">5</text><text x="200.4" y="163.9">8</text><text x="273.3" y="163.9">2</text>
|
||||
<text x="54.7" y="200.4">8</text><text x="127.5" y="200.4">3</text><text x="309.8" y="200.4">5</text>
|
||||
<text x="18.2" y="236.9">3</text><text x="91.1" y="236.9">5</text><text x="127.5" y="236.9">9</text><text x="200.4" y="236.9">6</text><text x="236.9" y="236.9">8</text><text x="273.3" y="236.9">7</text><text x="309.8" y="236.9">1</text>
|
||||
<text x="91.1" y="273.3">6</text><text x="273.3" y="273.3">4</text>
|
||||
<text x="91.1" y="309.8">1</text><text x="163.9" y="309.8">7</text><text x="236.9" y="309.8">2</text>
|
||||
</g>
|
||||
<g fill="#0c7f78" font-size="8.8" font-weight="800" text-anchor="middle" dominant-baseline="middle">
|
||||
<text x="163.9" y="154.8">2 4 6</text>
|
||||
<text x="163.9" y="171.5">7</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="flow">
|
||||
<div class="flow-step"><strong>Parse</strong><span>Read whitespace-separated rows and validate the shape.</span></div>
|
||||
<div class="flow-step"><strong>Model</strong><span>Precompute units and peers for each cell.</span></div>
|
||||
<div class="flow-step"><strong>Mask</strong><span>Track every candidate set as bits in a <span class="mono">u128</span>.</span></div>
|
||||
<div class="flow-step"><strong>Deduce</strong><span>Propagate placements, naked singles, hidden singles.</span></div>
|
||||
<div class="flow-step"><strong>Branch</strong><span>Pick the tightest unsolved cell and order values by impact.</span></div>
|
||||
<div class="flow-step"><strong>Solve</strong><span>Return the first complete state that survives constraints.</span></div>
|
||||
</div>
|
||||
<div class="page-number">01 / overview</div>
|
||||
</section>
|
||||
|
||||
<section class="page">
|
||||
<p class="kicker">1. Input becomes a constraint model</p>
|
||||
<h2>The grid is square, but the solver thinks in units and peers</h2>
|
||||
<p class="lead">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.</p>
|
||||
|
||||
<div class="two-col">
|
||||
<div>
|
||||
<div class="code">0 2 3 0
|
||||
0 0 0 1
|
||||
1 0 0 0
|
||||
0 4 2 0</div>
|
||||
<p class="caption">Blank cells are written as <span class="mono">0</span>. A 4x4 puzzle uses 2x2 blocks; a 9x9 puzzle uses 3x3 blocks; the same machinery handles both.</p>
|
||||
<div class="stats">
|
||||
<div class="stat"><b>3</b><span>unit kinds per cell</span></div>
|
||||
<div class="stat"><b>27</b><span>units in a 9x9 puzzle</span></div>
|
||||
<div class="stat"><b>20</b><span>peers for a 9x9 cell</span></div>
|
||||
<div class="stat"><b>1</b><span>state object to search</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<svg class="svg-frame" width="510" height="360" viewBox="0 0 510 360" aria-label="Row column block peers">
|
||||
<rect width="510" height="360" rx="8" fill="#fffaf7"/>
|
||||
<g transform="translate(32 30)">
|
||||
<g>
|
||||
<rect x="0" y="0" width="270" height="270" rx="7" fill="#fff"/>
|
||||
<g fill="#e3f4f0">
|
||||
<rect x="0" y="90" width="270" height="30"/>
|
||||
<rect x="120" y="0" width="30" height="270"/>
|
||||
</g>
|
||||
<rect x="90" y="90" width="90" height="90" fill="#ffe7b1"/>
|
||||
<rect x="120" y="90" width="30" height="30" fill="#df6e49"/>
|
||||
<g stroke="#c8d1d5" stroke-width="1">
|
||||
<path d="M30 0V270M60 0V270M120 0V270M150 0V270M210 0V270M240 0V270"/>
|
||||
<path d="M0 30H270M0 60H270M0 120H270M0 150H270M0 210H270M0 240H270"/>
|
||||
</g>
|
||||
<g stroke="#142236" stroke-width="3">
|
||||
<path d="M90 0V270M180 0V270M0 90H270M0 180H270"/>
|
||||
<rect width="270" height="270" fill="none"/>
|
||||
</g>
|
||||
<g fill="#142236" font-size="16" font-weight="800" text-anchor="middle" dominant-baseline="middle">
|
||||
<text x="135" y="105">cell</text>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(315 8)" font-size="14" font-weight="800">
|
||||
<rect x="0" y="0" width="140" height="58" rx="8" fill="#e3f4f0" stroke="#99cfc6"/>
|
||||
<text x="18" y="24" fill="#0c7f78">row peers</text>
|
||||
<text x="18" y="43" fill="#597080" font-size="11" font-weight="650">same horizontal unit</text>
|
||||
<rect x="0" y="78" width="140" height="58" rx="8" fill="#e3f4f0" stroke="#99cfc6"/>
|
||||
<text x="18" y="102" fill="#0c7f78">column peers</text>
|
||||
<text x="18" y="121" fill="#597080" font-size="11" font-weight="650">same vertical unit</text>
|
||||
<rect x="0" y="156" width="140" height="58" rx="8" fill="#ffe7b1" stroke="#e8bd58"/>
|
||||
<text x="18" y="180" fill="#835214">block peers</text>
|
||||
<text x="18" y="199" fill="#806743" font-size="11" font-weight="650">same sub-square</text>
|
||||
<rect x="0" y="234" width="140" height="36" rx="8" fill="#df6e49"/>
|
||||
<text x="18" y="257" fill="#fff">selected cell</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<p class="caption">Peers are precomputed once, so assigning a value later is just a quick walk over affected cells.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-number">02 / model</div>
|
||||
</section>
|
||||
|
||||
<section class="page">
|
||||
<p class="kicker">2. Candidates are bits, not lists</p>
|
||||
<h2>A cell’s possible values fit into one <span class="mono">u128</span></h2>
|
||||
<p class="lead">Each value maps to one bit. Eliminating a candidate clears its bit; placing a value replaces the whole mask with exactly that bit.</p>
|
||||
|
||||
<div class="two-col">
|
||||
<div>
|
||||
<svg class="svg-frame" width="470" height="390" viewBox="0 0 470 390" aria-label="Bit mask candidate diagram">
|
||||
<rect width="470" height="390" rx="8" fill="#fffaf7"/>
|
||||
<g transform="translate(32 34)">
|
||||
<text x="0" y="0" fill="#152033" font-size="18" font-weight="900">9x9 full mask</text>
|
||||
<text x="0" y="26" fill="#5d6c7d" font-size="13">values 1 through 9 are initially possible</text>
|
||||
<g transform="translate(0 56)">
|
||||
<g font-size="12" font-weight="850" text-anchor="middle">
|
||||
<text x="22" y="-12" fill="#506074">1</text><text x="68" y="-12" fill="#506074">2</text><text x="114" y="-12" fill="#506074">3</text>
|
||||
<text x="160" y="-12" fill="#506074">4</text><text x="206" y="-12" fill="#506074">5</text><text x="252" y="-12" fill="#506074">6</text>
|
||||
<text x="298" y="-12" fill="#506074">7</text><text x="344" y="-12" fill="#506074">8</text><text x="390" y="-12" fill="#506074">9</text>
|
||||
</g>
|
||||
<g>
|
||||
<rect x="0" y="0" width="44" height="44" rx="8" fill="#0c7f78"/><rect x="46" y="0" width="44" height="44" rx="8" fill="#0c7f78"/>
|
||||
<rect x="92" y="0" width="44" height="44" rx="8" fill="#0c7f78"/><rect x="138" y="0" width="44" height="44" rx="8" fill="#0c7f78"/>
|
||||
<rect x="184" y="0" width="44" height="44" rx="8" fill="#0c7f78"/><rect x="230" y="0" width="44" height="44" rx="8" fill="#0c7f78"/>
|
||||
<rect x="276" y="0" width="44" height="44" rx="8" fill="#0c7f78"/><rect x="322" y="0" width="44" height="44" rx="8" fill="#0c7f78"/>
|
||||
<rect x="368" y="0" width="44" height="44" rx="8" fill="#0c7f78"/>
|
||||
</g>
|
||||
<g fill="#fff" font-size="16" font-weight="900" text-anchor="middle" dominant-baseline="middle">
|
||||
<text x="22" y="22">1</text><text x="68" y="22">1</text><text x="114" y="22">1</text><text x="160" y="22">1</text><text x="206" y="22">1</text>
|
||||
<text x="252" y="22">1</text><text x="298" y="22">1</text><text x="344" y="22">1</text><text x="390" y="22">1</text>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 155)">
|
||||
<path d="M0 0H412" stroke="#d3d9dd" stroke-width="2"/>
|
||||
<text x="0" y="38" fill="#152033" font-size="18" font-weight="900">After peers place 1, 5, 8</text>
|
||||
<g transform="translate(0 66)">
|
||||
<g>
|
||||
<rect x="0" y="0" width="44" height="44" rx="8" fill="#dbe2e6"/><rect x="46" y="0" width="44" height="44" rx="8" fill="#0c7f78"/>
|
||||
<rect x="92" y="0" width="44" height="44" rx="8" fill="#0c7f78"/><rect x="138" y="0" width="44" height="44" rx="8" fill="#0c7f78"/>
|
||||
<rect x="184" y="0" width="44" height="44" rx="8" fill="#dbe2e6"/><rect x="230" y="0" width="44" height="44" rx="8" fill="#0c7f78"/>
|
||||
<rect x="276" y="0" width="44" height="44" rx="8" fill="#0c7f78"/><rect x="322" y="0" width="44" height="44" rx="8" fill="#dbe2e6"/>
|
||||
<rect x="368" y="0" width="44" height="44" rx="8" fill="#0c7f78"/>
|
||||
</g>
|
||||
<g fill="#fff" font-size="16" font-weight="900" text-anchor="middle" dominant-baseline="middle">
|
||||
<text x="68" y="22">1</text><text x="114" y="22">1</text><text x="160" y="22">1</text><text x="252" y="22">1</text><text x="298" y="22">1</text><text x="390" y="22">1</text>
|
||||
</g>
|
||||
<g fill="#7c8794" font-size="16" font-weight="900" text-anchor="middle" dominant-baseline="middle">
|
||||
<text x="22" y="22">0</text><text x="206" y="22">0</text><text x="344" y="22">0</text>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 298)">
|
||||
<rect x="0" y="0" width="412" height="45" rx="8" fill="#101827"/>
|
||||
<text x="18" y="29" fill="#f6efe4" font-size="15" font-family="monospace">mask & !value_bit(value)</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="card">
|
||||
<h3>Why masks work well here</h3>
|
||||
<p>Bit masks make candidate checks tiny: membership is <span class="mono">mask & bit != 0</span>, removal is <span class="mono">mask &= !bit</span>, and a naked single is <span class="mono">count_ones() == 1</span>.</p>
|
||||
</div>
|
||||
<div class="three-col" style="grid-template-columns: 1fr; gap: 3.5mm; margin-top: 4mm;">
|
||||
<div class="card">
|
||||
<h3>Full mask</h3>
|
||||
<p><span class="mono">(1 << size) - 1</span> turns on every legal value bit.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Placed value</h3>
|
||||
<p>Once a cell is assigned, its candidate mask becomes exactly <span class="mono">value_bit(value)</span>.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Contradiction</h3>
|
||||
<p>If an unsolved cell’s mask becomes zero, that path is rejected.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-number">03 / candidates</div>
|
||||
</section>
|
||||
|
||||
<section class="page">
|
||||
<p class="kicker">3. Deduction runs until it stalls</p>
|
||||
<h2>Every assignment immediately removes pressure from the board</h2>
|
||||
<p class="lead">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.</p>
|
||||
|
||||
<div class="two-col">
|
||||
<svg class="svg-frame" width="510" height="440" viewBox="0 0 510 440" aria-label="Deduction loop diagram">
|
||||
<rect width="510" height="440" rx="8" fill="#fffaf7"/>
|
||||
<g transform="translate(58 42)">
|
||||
<path d="M196 6C286 6 360 80 360 170C360 260 286 334 196 334C106 334 32 260 32 170C32 80 106 6 196 6Z" fill="#f1f6f4" stroke="#c8d7d3" stroke-width="2"/>
|
||||
<path d="M276 28C330 58 366 115 366 180" fill="none" stroke="#df6e49" stroke-width="8" stroke-linecap="round"/>
|
||||
<path d="M116 312C62 282 26 225 26 160" fill="none" stroke="#0c7f78" stroke-width="8" stroke-linecap="round"/>
|
||||
<polygon points="365,181 354,162 376,162" fill="#df6e49" transform="rotate(88 365 181)"/>
|
||||
<polygon points="27,159 16,178 38,178" fill="#0c7f78" transform="rotate(268 27 159)"/>
|
||||
|
||||
<g>
|
||||
<rect x="118" y="16" width="156" height="58" rx="10" fill="#101827"/>
|
||||
<text x="196" y="40" fill="#fffaf0" font-size="16" font-weight="900" text-anchor="middle">assign value</text>
|
||||
<text x="196" y="61" fill="#b6c6c4" font-size="12" text-anchor="middle">set value and exact mask</text>
|
||||
</g>
|
||||
<g>
|
||||
<rect x="266" y="140" width="168" height="64" rx="10" fill="#e3f4f0" stroke="#99cfc6"/>
|
||||
<text x="350" y="166" fill="#0c7f78" font-size="16" font-weight="900" text-anchor="middle">propagate</text>
|
||||
<text x="350" y="187" fill="#557071" font-size="12" text-anchor="middle">clear this value from peers</text>
|
||||
</g>
|
||||
<g>
|
||||
<rect x="118" y="266" width="156" height="64" rx="10" fill="#ffe7b1" stroke="#e7bd58"/>
|
||||
<text x="196" y="292" fill="#7a4c13" font-size="16" font-weight="900" text-anchor="middle">naked singles</text>
|
||||
<text x="196" y="313" fill="#806743" font-size="12" text-anchor="middle">one remaining candidate</text>
|
||||
</g>
|
||||
<g>
|
||||
<rect x="-30" y="140" width="168" height="64" rx="10" fill="#fbe5dd" stroke="#dfb29f"/>
|
||||
<text x="54" y="166" fill="#a74428" font-size="16" font-weight="900" text-anchor="middle">hidden singles</text>
|
||||
<text x="54" y="187" fill="#76564a" font-size="12" text-anchor="middle">one place in a unit</text>
|
||||
</g>
|
||||
<circle cx="196" cy="170" r="46" fill="#fff" stroke="#cdd7da" stroke-width="2"/>
|
||||
<text x="196" y="162" fill="#152033" font-size="15" font-weight="900" text-anchor="middle">repeat</text>
|
||||
<text x="196" y="184" fill="#5e7080" font-size="12" text-anchor="middle">until stable</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<div>
|
||||
<div class="card">
|
||||
<h3>Naked single</h3>
|
||||
<p>A cell with one remaining candidate is forced. The solver detects this with <span class="mono">count_ones() == 1</span>, assigns it, and propagates again.</p>
|
||||
</div>
|
||||
<div class="card" style="margin-top: 6mm;">
|
||||
<h3>Hidden single</h3>
|
||||
<p>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.</p>
|
||||
</div>
|
||||
<div class="callout">If a unit has no legal place for a value, or a candidate mask goes empty, that path is rejected as a contradiction.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-number">04 / deduction</div>
|
||||
</section>
|
||||
|
||||
<section class="page">
|
||||
<p class="kicker">4. Search begins only after deduction stalls</p>
|
||||
<h2>The branch point is chosen to make guessing as constrained as possible</h2>
|
||||
<p class="lead">When no rule can place another value, the solver picks one unresolved cell and tries each candidate in a cloned state.</p>
|
||||
|
||||
<div class="two-col">
|
||||
<div>
|
||||
<svg class="svg-frame" width="510" height="415" viewBox="0 0 510 415" aria-label="Branch cell selection diagram">
|
||||
<rect width="510" height="415" rx="8" fill="#fffaf7"/>
|
||||
<g transform="translate(34 32)">
|
||||
<text x="0" y="0" fill="#152033" font-size="18" font-weight="900">Choose branch cell</text>
|
||||
<text x="0" y="25" fill="#607080" font-size="13">Fewest candidates wins; ties prefer more unsolved peers.</text>
|
||||
<g transform="translate(0 55)">
|
||||
<rect x="0" y="0" width="110" height="110" rx="8" fill="#e3f4f0" stroke="#99cfc6"/>
|
||||
<text x="55" y="38" fill="#0c7f78" font-size="24" font-weight="900" text-anchor="middle">{2,7}</text>
|
||||
<text x="55" y="68" fill="#405064" font-size="13" font-weight="800" text-anchor="middle">2 candidates</text>
|
||||
<text x="55" y="91" fill="#607080" font-size="11" text-anchor="middle">13 open peers</text>
|
||||
<rect x="134" y="0" width="110" height="110" rx="8" fill="#ffffff" stroke="#ccd5d9"/>
|
||||
<text x="189" y="38" fill="#334357" font-size="21" font-weight="900" text-anchor="middle">{1,5,9}</text>
|
||||
<text x="189" y="68" fill="#405064" font-size="13" font-weight="800" text-anchor="middle">3 candidates</text>
|
||||
<text x="189" y="91" fill="#607080" font-size="11" text-anchor="middle">18 open peers</text>
|
||||
<rect x="268" y="0" width="110" height="110" rx="8" fill="#ffe7b1" stroke="#e7bd58"/>
|
||||
<text x="323" y="38" fill="#7a4c13" font-size="24" font-weight="900" text-anchor="middle">{4,6}</text>
|
||||
<text x="323" y="68" fill="#405064" font-size="13" font-weight="800" text-anchor="middle">2 candidates</text>
|
||||
<text x="323" y="91" fill="#607080" font-size="11" text-anchor="middle">17 open peers</text>
|
||||
<text x="323" y="131" fill="#df6e49" font-size="13" font-weight="900" text-anchor="middle">selected</text>
|
||||
</g>
|
||||
<g transform="translate(0 230)">
|
||||
<text x="0" y="0" fill="#152033" font-size="18" font-weight="900">Order selected candidates</text>
|
||||
<text x="0" y="25" fill="#607080" font-size="13">Impact = peers that would lose this value as a candidate.</text>
|
||||
<g transform="translate(0 52)">
|
||||
<rect x="0" y="0" width="390" height="34" rx="8" fill="#f0f3f5"/>
|
||||
<rect x="0" y="0" width="326" height="34" rx="8" fill="#df6e49"/>
|
||||
<text x="13" y="22" fill="#fff" font-size="13" font-weight="900">try 6 first</text>
|
||||
<text x="372" y="22" fill="#334357" font-size="13" font-weight="900" text-anchor="end">17</text>
|
||||
</g>
|
||||
<g transform="translate(0 98)">
|
||||
<rect x="0" y="0" width="390" height="34" rx="8" fill="#f0f3f5"/>
|
||||
<rect x="0" y="0" width="231" height="34" rx="8" fill="#0c7f78"/>
|
||||
<text x="13" y="22" fill="#fff" font-size="13" font-weight="900">then 4</text>
|
||||
<text x="372" y="22" fill="#334357" font-size="13" font-weight="900" text-anchor="end">12</text>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="card">
|
||||
<h3>Cell heuristic</h3>
|
||||
<p>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.</p>
|
||||
</div>
|
||||
<div class="card" style="margin-top: 6mm;">
|
||||
<h3>Candidate heuristic</h3>
|
||||
<p>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.</p>
|
||||
</div>
|
||||
<div class="card" style="margin-top: 6mm;">
|
||||
<h3>Tie break</h3>
|
||||
<p>If candidate impacts are equal, the smaller value is tried first for deterministic output.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-number">05 / search</div>
|
||||
</section>
|
||||
|
||||
<section class="page">
|
||||
<p class="kicker">5. Backtracking is just recursive state cloning</p>
|
||||
<h2>Every assumption either solves the board or collapses quickly</h2>
|
||||
<p class="lead">Search clones the current state, assigns one candidate, and immediately re-enters deduction. Failed branches vanish; successful branches return the solved grid.</p>
|
||||
|
||||
<div class="two-col">
|
||||
<svg class="svg-frame" width="515" height="410" viewBox="0 0 515 410" aria-label="Backtracking tree">
|
||||
<rect width="515" height="410" rx="8" fill="#fffaf7"/>
|
||||
<g transform="translate(44 38)">
|
||||
<rect x="145" y="0" width="150" height="58" rx="10" fill="#101827"/>
|
||||
<text x="220" y="25" fill="#fffaf0" font-size="15" font-weight="900" text-anchor="middle">deduced state</text>
|
||||
<text x="220" y="45" fill="#b7c6c5" font-size="11" text-anchor="middle">not solved, no forced moves</text>
|
||||
|
||||
<path d="M220 58C220 92 118 99 118 132" fill="none" stroke="#b7c2c8" stroke-width="2"/>
|
||||
<path d="M220 58C220 92 322 99 322 132" fill="none" stroke="#b7c2c8" stroke-width="2"/>
|
||||
|
||||
<rect x="38" y="132" width="160" height="60" rx="10" fill="#fbe5dd" stroke="#dfb29f"/>
|
||||
<text x="118" y="157" fill="#a74428" font-size="15" font-weight="900" text-anchor="middle">try value 6</text>
|
||||
<text x="118" y="178" fill="#76564a" font-size="11" text-anchor="middle">contradiction during deduction</text>
|
||||
|
||||
<rect x="242" y="132" width="160" height="60" rx="10" fill="#e3f4f0" stroke="#99cfc6"/>
|
||||
<text x="322" y="157" fill="#0c7f78" font-size="15" font-weight="900" text-anchor="middle">try value 4</text>
|
||||
<text x="322" y="178" fill="#557071" font-size="11" text-anchor="middle">deduction continues</text>
|
||||
|
||||
<path d="M322 192C322 226 232 232 232 264" fill="none" stroke="#b7c2c8" stroke-width="2"/>
|
||||
<path d="M322 192C322 226 411 232 411 264" fill="none" stroke="#b7c2c8" stroke-width="2"/>
|
||||
|
||||
<rect x="152" y="264" width="160" height="60" rx="10" fill="#ffe7b1" stroke="#e7bd58"/>
|
||||
<text x="232" y="289" fill="#7a4c13" font-size="15" font-weight="900" text-anchor="middle">more branching</text>
|
||||
<text x="232" y="310" fill="#806743" font-size="11" text-anchor="middle">same rules, deeper tree</text>
|
||||
|
||||
<rect x="330" y="264" width="130" height="60" rx="10" fill="#0c7f78"/>
|
||||
<text x="395" y="289" fill="#fff" font-size="13" font-weight="900" text-anchor="middle">complete grid</text>
|
||||
<text x="395" y="310" fill="#d5eeea" font-size="10.5" text-anchor="middle">return solution</text>
|
||||
|
||||
<g stroke="#df6e49" stroke-width="4" stroke-linecap="round">
|
||||
<path d="M78 214L158 244"/>
|
||||
<path d="M158 214L78 244"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<div>
|
||||
<div class="card">
|
||||
<h3>The core rhythm</h3>
|
||||
<ul>
|
||||
<li>Run deduction before every search decision.</li>
|
||||
<li>If all cells are filled, return the state.</li>
|
||||
<li>Otherwise choose one branch cell.</li>
|
||||
<li>Try candidates in impact order.</li>
|
||||
<li>Discard any branch that hits a contradiction.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="callout"><span class="pill">Key idea</span><br>Most of the “intelligence” is in shrinking the search tree before guessing and making each guess remove as much uncertainty as possible.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-number">06 / backtracking</div>
|
||||
</section>
|
||||
|
||||
<section class="page dark">
|
||||
<p class="kicker">Implementation map</p>
|
||||
<h2>Where to look in the code</h2>
|
||||
<p class="lead">The solver is small enough that the visual model maps directly onto a few functions in <span class="mono">src/lib.rs</span>.</p>
|
||||
|
||||
<div class="three-col">
|
||||
<div class="card">
|
||||
<h3>Parsing</h3>
|
||||
<p><span class="mono">Puzzle::parse</span> validates square dimensions, block layout, and value ranges before solving starts.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Constraint model</h3>
|
||||
<p><span class="mono">Solver::new</span> creates row, column, and block units; <span class="mono">build_peers</span> turns them into peer lists.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Candidate masks</h3>
|
||||
<p><span class="mono">full_mask</span> and <span class="mono">value_bit</span> encode possible values into a single integer.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Propagation</h3>
|
||||
<p><span class="mono">assign</span> places a value, checks duplicate peers, and clears that value from every unsolved peer.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Deduction</h3>
|
||||
<p><span class="mono">deduce</span> repeatedly applies naked singles and hidden singles until no rule progresses.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Search</h3>
|
||||
<p><span class="mono">search</span> clones states, tries ordered candidates, and returns the first branch that reaches a complete solution.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flow" style="grid-template-columns: repeat(4, 1fr); margin-top: 18mm;">
|
||||
<div class="flow-step"><strong>Correctness guard</strong><span>Contradictions stop invalid givens and impossible branches early.</span></div>
|
||||
<div class="flow-step"><strong>Performance trick</strong><span>Peer lists and bit masks make the hot path small.</span></div>
|
||||
<div class="flow-step"><strong>Search control</strong><span>Fewest candidates narrows the branch factor.</span></div>
|
||||
<div class="flow-step"><strong>Impact ordering</strong><span>Higher-impact values test stronger assumptions first.</span></div>
|
||||
</div>
|
||||
<div class="page-number">07 / code map</div>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
Reference in New Issue
Block a user