<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://blog.morefreeze.top/feed.xml" rel="self" type="application/atom+xml" /><link href="https://blog.morefreeze.top/" rel="alternate" type="text/html" /><updated>2026-06-09T12:18:06+00:00</updated><id>https://blog.morefreeze.top/feed.xml</id><title type="html">MoreFreeze’s Sanctuary</title><subtitle>后端工程师的技术博客，专注组合算法（TAOCP）、Go 语言开发、分布式系统与 Vibe Coding 实践</subtitle><author><name>More Freeze</name></author><entry><title type="html">Kakuro：填字游戏遇上组合数学</title><link href="https://blog.morefreeze.top/2026/04/kakuro.html" rel="alternate" type="text/html" title="Kakuro：填字游戏遇上组合数学" /><published>2026-04-23T00:00:00+00:00</published><updated>2026-04-23T00:00:00+00:00</updated><id>https://blog.morefreeze.top/2026/04/kakuro</id><content type="html" xml:base="https://blog.morefreeze.top/2026/04/kakuro.html"><![CDATA[<p>你一定见过填字游戏：一个方格，空白处填字母，横竖都要拼成单词。Kakuro 是它的数字版本——格子里填 1 到 9 的数字，横向和纵向的每段连续格子（称为一个 block）都要满足指定的求和约束，并且同一个 block 内的数字不能重复。</p>

<p>规则就这三条：</p>
<ol>
  <li>每个白格填 1-9 中的一个数字</li>
  <li>同一 block 内数字不重复</li>
  <li>每个 block 的数字之和等于线索给出的目标值</li>
</ol>

<p>下面这个小谜题展示了这些要素：</p>

<p><img src="/images/kakuro-mini.svg" alt="一个小型 Kakuro 谜题示例" style="width:100%; max-width:600px;" /></p>

<p>黑格上的斜线数字就是线索：斜线右下方是横向线索，左下方是纵向线索。</p>

<h2 id="手推一遍">手推一遍</h2>

<p>用一个 3 行 × 2 列白格的教学例题来说明推导过程：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>        col1  col2
row2:    ?     ?    ← H0 横向线索 sum=3
row3:    ?     ?    ← H1 横向线索 sum=11
row4:    ?     ?    ← H2 横向线索 sum=4
         ↑     ↑
        V0=7  V1=11
</code></pre></div></div>

<p><strong>第一步：</strong> 纵向 V0 的 sum=7，长度 3，digits 1-9 不重复。枚举一下：\(\{1,2,4\}\) 和 \(\{1,3,3\}\)（重复，不合法）……其实只有 \(\{1,2,4\}\) 满足条件。这是一个”魔法块”——数字集合完全确定。</p>

<p><strong>第二步：</strong> 横向 H0 的 sum=3，长度 2，唯一组合是 \(\{1,2\}\)——又是魔法块。</p>

<p><strong>第三步：</strong> 格子 \((2,1)\) 处于 V0 和 H0 的交叉。V0 的数字集合 \(\{1,2,4\}\)，H0 的数字集合 \(\{1,2\}\)，交集是 \(\{1,2\}\)，所以 \((2,1)\) 填 1 或 2。</p>

<p><strong>第四步：</strong> H2 的 sum=4，长度 2，唯一组合是 \(\{1,3\}\)。格子 \((4,1)\) 也在 V0 里，V0 已经确定数字集合是 \(\{1,2,4\}\)，与 H2 的 \(\{1,3\}\) 取交集得 \(\{1\}\)。所以 \((4,1)=1\)。</p>

<p><strong>第五步：</strong> 由 \((4,1)=1\) 知 H2 的另一格 \((4,2)=3\) （因为 1+3=4）。V0 剩余的两格 \((2,1)+(3,1)=7-1=6\)，且数字来自 \(\{1,2,4\}\setminus\{1\}=\{2,4\}\)，所以 \((2,1)=2,(3,1)=4\) 或反过来。</p>

<p><strong>第六步：</strong> H0 sum=3，已知 \((2,1)=2\) 的话 \((2,2)=1\)；若 \((2,1)=4\) 则 \((2,2)=-1\) 不合法。于是唯一解是 \((2,1)=2,(2,2)=1\)，从而 \((3,1)=4\)。</p>

<p><strong>第七步：</strong> V1 sum=11，格子 \((2,2)+(3,2)+(4,2)=1+(3,2)+3=11\)，所以 \((3,2)=7\)。验证 H1：\(4+7=11\)，正确。</p>

<p>唯一解：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>2  1
4  7
1  3
</code></pre></div></div>

<p>人能推导，计算机如何系统化？</p>

<!--more-->

<hr />

<h2 id="sum-table组合数学的基石">Sum Table——组合数学的基石</h2>

<p>在手推过程中，关键步骤是”找出和为 n、长度 k 的所有合法数字集合”。Kakuro 求解器需要对所有可能的 \((n, k)\) 预先建好这张表——这就是 <strong>Sum Table</strong>。</p>

<h3 id="n-in-k-问题">n-in-k 问题</h3>

<p>正式定义：给定目标和 \(n\)（1 到 45）和格子数 \(k\)（1 到 9），找出所有从 \(\{1,\ldots,9\}\) 中选 \(k\) 个不同数字、使其之和等于 \(n\) 的子集。</p>

<h3 id="bitmap-枚举">Bitmap 枚举</h3>

<p>Knuth 用位掩码（bitmask）来枚举：用一个 9 位整数 \(x\) 表示哪些数字被选中，第 \(i\) 位（从 0 开始）为 1 表示选了数字 \(i+1\)。</p>

<ul>
  <li>\(x=0b000000011=3\) 表示 \(\{1,2\}\)，长度 2，和 3</li>
  <li>\(x=0b000000101=5\) 表示 \(\{1,3\}\)，长度 2，和 4</li>
  <li>\(x=0b111111111=511\) 表示 \(\{1,2,\ldots,9\}\)，长度 9，和 45</li>
</ul>

<p>遍历所有 \(3 \le x &lt; 512\)（至少两位，最多九位），按 \((n, k)\) 分组，核心代码只需几行：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">for</span> <span class="n">mask</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">3</span><span class="p">,</span> <span class="mi">512</span><span class="p">):</span>
    <span class="n">count</span> <span class="o">=</span> <span class="nb">bin</span><span class="p">(</span><span class="n">mask</span><span class="p">).</span><span class="n">count</span><span class="p">(</span><span class="s">'1'</span><span class="p">)</span>
    <span class="n">sum_val</span> <span class="o">=</span> <span class="nb">sum</span><span class="p">(</span><span class="n">i</span> <span class="o">+</span> <span class="mi">1</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">9</span><span class="p">)</span> <span class="k">if</span> <span class="n">mask</span> <span class="o">&amp;</span> <span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="n">i</span><span class="p">))</span>
    <span class="n">C</span><span class="p">[(</span><span class="n">sum_val</span><span class="p">,</span> <span class="n">count</span><span class="p">)].</span><span class="n">append</span><span class="p">(</span><span class="n">mask</span><span class="p">)</span>
</code></pre></div></div>

<p>位运算天然保证了数字不重复（每位只有 0 或 1），枚举的总次数只有 \(2^9 - 3 = 509\) 次（排除了空集 0、单元素集 1 和 2），非常高效。</p>

<h3 id="数据概览">数据概览</h3>

<p>运行 <code class="language-plaintext highlighter-rouge">build_sum_table()</code> 之后：</p>

<ul>
  <li><strong>总共 127 个 \((n, k)\) 对</strong>有合法组合</li>
  <li><strong>41 个 magic block</strong>：这些 \((n, k)\) 对只有唯一一种数字组合</li>
  <li><strong>最多 12 种组合</strong>，出现在 \((20, 4)\) 和 \((25, 5)\) 两处</li>
</ul>

<h3 id="magic-block解题的突破口">Magic Block：解题的突破口</h3>

<table>
  <tbody>
    <tr>
      <td>当 $$</td>
      <td>C(n,k)</td>
      <td>=1$$ 时，这个 block 的数字集合完全确定，是约束传播的最佳起点。最极端的两个例子：</td>
    </tr>
  </tbody>
</table>

<ul>
  <li>\((3, 2)\)：只有 \(\{1,2\}\)（1+2=3 是两个不同数字之和的最小值）</li>
  <li>\((45, 9)\)：只有 \(\{1,2,3,4,5,6,7,8,9\}\)（九格必须填满所有数字）</li>
</ul>

<p>完整的 41 个 magic block 构成了 Kakuro 求解的”免费约束”库。</p>

<h3 id="dual-性质">Dual 性质</h3>

<p>Sum Table 有一个优雅的对称性：如果把每个数字 \(d\) 替换为 \(10-d\)，一个和为 \(s\)、长度 \(k\) 的合法组合就变成了和为 \(10k-s\)、长度 \(k\) 的合法组合。两个 \((n, k)\) 和 \((10k-n, k)\) 的组合数完全相同，合法组合之间存在一一对应关系。这个对偶性意味着 Sum Table 天然关于 \(n=5k\) 对称。</p>

<p>下图是 Sum Table 的热图，颜色越深表示该 \((n, k)\) 的组合数越多：</p>

<p><img src="/images/kakuro-sum-table.svg" alt="Sum Table 热图：各 (sum, length) 对应的组合数量" style="width:100%; max-width:560px;" /></p>

<hr />

<h2 id="xcc-编码把-kakuro-变成精确覆盖问题">XCC 编码——把 Kakuro 变成精确覆盖问题</h2>

<p>有了 Sum Table，下一步是把 Kakuro 翻译成 XCC（带颜色的精确覆盖）问题，交给 Algorithm C 求解。</p>

<h3 id="朴素想法的问题">朴素想法的问题</h3>

<p>最直接的想法：把每个 block 的所有排列都列举出来。一个 4 格 block 的组合可能有 12 种数字集合，每种集合有 \(4!=24\) 种排列，共 288 个选项；如果有多个这样的 block，选项数量组合爆炸。</p>

<p>Knuth 在 TAOCP 4B Exercise 430 的答案（answer 430d）给出了一个高效编码，利用 XCC 的颜色机制大幅压缩。</p>

<h3 id="knuth-高效编码">Knuth 高效编码</h3>

<p><strong>Primary items</strong>（主要项）：每个白格 \(c_{ij}\) 对应一个主要项，必须被精确覆盖一次，表示”这个格子恰好填一个数字”。</p>

<p><strong>Colored secondary items</strong>（带颜色的次要项）：对每个 block \(B\)，每种可能的数字集合（bitmask）\(p\) 对应一个次要项 \(\langle B, p\rangle\)，其颜色表示该格子实际填入的数字。</p>

<p><strong>Options</strong>（选项）：对每个格子 \((i,j)\)，设其所在横向 block 为 \(B_h\) （可能的 bitmask 集合为 \(P\)），纵向 block 为 \(B_v\)（可能的 bitmask 集合为 \(Q\)）。对每对 \((p \in P, q \in Q)\) 及其交叉数字 \(x \in p \cap q\)，生成一个选项：</p>

\[c_{ij} \quad \langle B_h, p\rangle \mathbin{:} x \quad \langle B_v, q\rangle \mathbin{:} x\]

<p>这里”\(\langle B, p\rangle \mathbin{:} x\)“表示次要项 \(\langle B, p\rangle\) 被赋予颜色 \(x\)。</p>

<p><strong>Absorber 选项</strong>：对每个次要项 \(\langle B, p\rangle\)，生成一个吸收器选项：</p>

\[\langle B, p\rangle \mathbin{:} 0\]

<p>吸收器的作用是处理”未被任何格子选中的 bitmask”——XCC 要求每个次要项都必须有确定的颜色，吸收器给它一个特殊颜色 0 表示”此组合未被选用”。</p>

<h3 id="颜色的关键作用">颜色的关键作用</h3>

<p>为什么把格子里的数字 \(x\) 作为颜色？</p>

<p>同一个格子 \((i,j)\) 属于横向 block \(B_h\) 和纵向 block \(B_v\)。从 \(B_h\) 的视角生成的选项携带颜色 \(x\)，从 \(B_v\) 的视角生成的选项也携带颜色 \(x\)——XCC 的颜色一致性约束自动保证：如果选了 \(B_h\) 的 bitmask \(p\) 且格子 \((i,j)\) 填数字 \(x\)，那么 \(B_v\) 也必须使用含 \(x\) 的 bitmask \(q\)，且在格子 \((i,j)\) 处同样填 \(x\).</p>

<p>这就是关键：<strong>用颜色表示数字，XCC 自动保证横纵 block 在交叉格的一致性</strong>，完全不需要任何额外的约束代码。</p>

<h3 id="教学例题的完整编码">教学例题的完整编码</h3>

<p>回到 3×2 的教学例题，逐步列出所有 items 和 options：</p>

<p><strong>Primary items（6个）：</strong> \(c_{2,1},\ c_{2,2},\ c_{3,1},\ c_{3,2},\ c_{4,1},\ c_{4,2}\)</p>

<p><strong>Secondary items：</strong></p>

<ul>
  <li>H0 sum=3, len=2 → 唯一 bitmask \(\{1,2\}\) → 1 个次要项 \(\langle H0, 0\rangle\)</li>
  <li>H1 sum=11, len=2 → 4 种 bitmask（\(\{2,9\},\{3,8\},\{4,7\},\{5,6\}\)）→ 4 个次要项 \(\langle H1, 0\rangle \ldots \langle H1, 3\rangle\)</li>
  <li>H2 sum=4, len=2 → 唯一 bitmask \(\{1,3\}\) → 1 个次要项 \(\langle H2, 0\rangle\)</li>
  <li>V0 sum=7, len=3 → 唯一 bitmask \(\{1,2,4\}\) → 1 个次要项 \(\langle V0, 0\rangle\)</li>
  <li>V1 sum=11, len=3 → 5 种 bitmask（\(\{1,2,8\},\{1,3,7\},\{1,4,6\},\{2,3,6\},\{2,4,5\}\)）→ 5 个次要项 \(\langle V1, 0\rangle \ldots \langle V1, 4\rangle\)</li>
</ul>

<p><strong>格子 \((2,1)\) 的选项：</strong> H0 唯一 bitmask \(\{1,2\}\) 与 V0 唯一 bitmask \(\{1,2,4\}\) 的交集是 \(\{1,2\}\)，生成：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>c_{2,1}  &lt;H0,0&gt;:1  &lt;V0,0&gt;:1    （格子填 1）
c_{2,1}  &lt;H0,0&gt;:2  &lt;V0,0&gt;:2    （格子填 2）
</code></pre></div></div>

<p><strong>格子 \((2,2)\) 的选项：</strong> H0 bitmask \(\{1,2\}\) 与 V1 的 5 种 bitmask 分别取交集：</p>

<ul>
  <li>\(\{1,2\} \cap \{1,2,8\} = \{1,2\}\) → 生成 \(c_{2,2}\ \langle H0,0\rangle\mathbin{:}1\ \langle V1,0\rangle\mathbin{:}1\) 和 \(c_{2,2}\ \langle H0,0\rangle\mathbin{:}2\ \langle V1,0\rangle\mathbin{:}2\)</li>
  <li>\(\{1,2\} \cap \{1,3,7\} = \{1\}\) → 生成 \(c_{2,2}\ \langle H0,0\rangle\mathbin{:}1\ \langle V1,1\rangle\mathbin{:}1\)</li>
  <li>\(\{1,2\} \cap \{1,4,6\} = \{1\}\) → 生成 \(c_{2,2}\ \langle H0,0\rangle\mathbin{:}1\ \langle V1,2\rangle\mathbin{:}1\)</li>
  <li>\(\{1,2\} \cap \{2,3,6\} = \{2\}\) → 生成 \(c_{2,2}\ \langle H0,0\rangle\mathbin{:}2\ \langle V1,3\rangle\mathbin{:}2\)</li>
  <li>\(\{1,2\} \cap \{2,4,5\} = \{2\}\) → 生成 \(c_{2,2}\ \langle H0,0\rangle\mathbin{:}2\ \langle V1,4\rangle\mathbin{:}2\)</li>
</ul>

<p>其他格子类似，再加上每个次要项各自的 absorber 选项。</p>

<p><strong>Absorber 选项（共 12 个）：</strong> \(\langle H0,0\rangle\mathbin{:}0,\ \langle H1,0\rangle\mathbin{:}0,\ \ldots,\ \langle V1,4\rangle\mathbin{:}0\)</p>

<p>XCC 求解器最终会选出唯一一组覆盖所有主要项、颜色一致的选项，对应唯一解 \(2,1,4,7,1,3\)。</p>

<hr />

<h2 id="编码实现">编码实现</h2>

<p>代码结构非常清晰，三步完成：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">C</span> <span class="o">=</span> <span class="n">build_sum_table</span><span class="p">()</span>          <span class="c1"># 预计算 Sum Table
</span><span class="n">puzzle</span> <span class="o">=</span> <span class="n">KakuroPuzzle</span><span class="p">.</span><span class="n">from_teaching_example</span><span class="p">()</span>
<span class="n">solutions</span> <span class="o">=</span> <span class="n">solve_kakuro</span><span class="p">(</span><span class="n">puzzle</span><span class="p">,</span> <span class="n">C</span><span class="p">)</span>   <span class="c1"># XCC 求解
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">solve_kakuro()</code> 内部调用 <code class="language-plaintext highlighter-rouge">solve_kakuro_with_labels()</code> 将谜题翻译成 XCC 格式，再交给 <code class="language-plaintext highlighter-rouge">DLX_C</code>（Algorithm C 的实现）搜索，最后把解码结果转换成 \((r, c) \to \text{digit}\) 的字典。实现采用了与上节略有差异的变体：每个 block 用单个次要项（颜色 = 排列索引）而非每个组合一个次要项，无需 absorber，两者逻辑等价。</p>

<p><code class="language-plaintext highlighter-rouge">build_sum_table()</code> 的核心循环：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">build_sum_table</span><span class="p">():</span>
    <span class="n">C</span> <span class="o">=</span> <span class="p">{}</span>
    <span class="k">for</span> <span class="n">mask</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">3</span><span class="p">,</span> <span class="mi">512</span><span class="p">):</span>
        <span class="n">count</span> <span class="o">=</span> <span class="nb">bin</span><span class="p">(</span><span class="n">mask</span><span class="p">).</span><span class="n">count</span><span class="p">(</span><span class="s">'1'</span><span class="p">)</span>
        <span class="n">sum_val</span> <span class="o">=</span> <span class="nb">sum</span><span class="p">(</span><span class="n">i</span> <span class="o">+</span> <span class="mi">1</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">9</span><span class="p">)</span> <span class="k">if</span> <span class="n">mask</span> <span class="o">&amp;</span> <span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="n">i</span><span class="p">))</span>
        <span class="n">key</span> <span class="o">=</span> <span class="p">(</span><span class="n">sum_val</span><span class="p">,</span> <span class="n">count</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">key</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">C</span><span class="p">:</span>
            <span class="n">C</span><span class="p">[</span><span class="n">key</span><span class="p">]</span> <span class="o">=</span> <span class="p">[]</span>
        <span class="n">C</span><span class="p">[</span><span class="n">key</span><span class="p">].</span><span class="n">append</span><span class="p">(</span><span class="n">mask</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">C</span>
</code></pre></div></div>

<p>运行教学例题，输出：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>求解结果：找到 1 个解

  2  1
  4  7
  1  3
</code></pre></div></div>

<p>Mini Kakuro（2×2 白格，线索 H0=3, H1=7, V0=4, V1=6）：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  1  2
  3  4
</code></pre></div></div>

<p>验证：H0: 1+2=3，H1: 3+4=7，V0: 1+3=4，V1: 2+4=6，全部正确。</p>

<p>完整代码见 <a href="https://github.com/morefreeze/morefreeze.github.io/blob/master/code/kakuro.py">kakuro.py</a>，依赖同目录下的 <code class="language-plaintext highlighter-rouge">dlx_colors.py</code>，直接运行即可。</p>

<hr />

<h2 id="pentomino-kakuro跨界的彩蛋">Pentomino Kakuro——跨界的彩蛋</h2>

<p>Kakuro 本身已经足够有趣，但 Knuth 在 TAOCP 4B Exercise 402 还藏了一道彩蛋：<strong>Pentomino Kakuro</strong>。</p>

<p>谜题在一个 12×12 的格子上进行。格子里被划分成若干个 “cage”——每个 cage 的形状恰好是一种五格骨牌（pentomino）。12 种标准五格骨牌中，Knuth 选用了 10 个（F、I、L、P、T、U、W、X、Y、Z），每个骨牌形状对应一个 cage，cage 的名字就是骨牌的字母。</p>

<p>每个 cage 给出一个约束：cage 内所有格子的数字之和（或乘积）等于线索值，且 cage 内数字不重复。解题规则和标准 Kakuro 完全相同，只是 block 的形状从矩形条变成了任意五格骨牌形状。</p>

<p><img src="/images/kakuro-pentomino.svg" alt="Pentomino Kakuro：每个 cage 形如一种五格骨牌" style="width:100%; max-width:576px;" /></p>

<p>这道谜题把两个完全不同领域的美丽事物拼在了一起：<a href="/2026/03/pentomino-tiling.html">五格骨牌的形状约束</a>和 Kakuro 的数字求和约束。我们在<a href="/2026/04/pentomino-coloring.html">五格骨牌三着色</a>一文里看到过骨牌的组合结构；这里，每个骨牌形状同时也是一道数字谜题的线索——形状即线索，视觉美与组合美融为一体。</p>

<p>XCC 编码完全可以处理这种不规则 cage：把每个 cage 的白格坐标列出来，把 cage 内的 \((n, k)\) 约束编码成 sum table 查询，其余步骤与标准 Kakuro 相同。</p>

<hr />

<h2 id="谜题设计什么样的-kakuro-是好题">谜题设计——什么样的 Kakuro 是好题？</h2>

<p>解题算法很直接，但设计一道好的 Kakuro 谜题却非易事。</p>

<h3 id="随机填数很少唯一">随机填数很少唯一</h3>

<p>Knuth 做过实验：随机取一张合法填写的 Kakuro 网格，统计满足这组线索的解的数量，平均大约有 <strong>75 个解</strong>，极少数情况下才能得到唯一解。谜题设计者需要精心选择线索和网格形状，才能让解唯一。</p>

<h3 id="magic-block-的价值">Magic Block 的价值</h3>

<p>好题的设计往往从 magic block 出发。41 个 magic block 是约束传播的起点：如果谜题里包含若干 magic block，解题者（人或算法）可以立刻锁定这些 block 的数字集合，然后向周围扩散约束。缺少 magic block 的谜题往往有大量解，难以构成有趣的谜题。</p>

<h3 id="对称性陷阱">对称性陷阱</h3>

<p>对称设计的谜题几乎总有对称解。如果把所有数字 \(d\) 替换为 \(10-d\)，整个谜题依然合法（利用前面提到的 dual 性质）。因此，纯粹对称的网格布局会产生至少两个解，除非额外约束打破这种对称。</p>

<h3 id="用-pi-出题">用 \(\pi\) 出题</h3>

<p>Knuth 在 Exercise 435 里展示了一个用 \(\pi\) 设计 Kakuro 的方法：取 \(\pi = 3.14159265\ldots\) 的数字，用 31、41、59、26、53、58、97 作为线索值，构造一张有趣的谜题。\(\pi\) 的数字本身没有规律性，使得线索分布均匀，不容易被人直接推测出来。这类”数学常数 Kakuro”既有视觉上的趣味，也保证了足够的约束强度。</p>

<h3 id="起源">起源</h3>

<p>Kakuro 最早出现在 1966 年 Jacob E. Funk 在 <em>Dell Pencil Puzzles and Word Games</em> 上发表的谜题中，当时叫做 “Cross Sums”。经过几十年演变，日本益智杂志将其推广为 Kakuro 这个名字（”加法”+”クロス（Cross）”的缩写），并在 2000 年代随数独热潮一起风靡全球。</p>

<h3 id="kakuro-的纯粹之处">Kakuro 的纯粹之处</h3>

<p>回头看这一切：Kakuro 的规则极简，却能产生层次丰富的推导链。它不依赖语言（不像填字游戏），不依赖记忆（不像数独的某些变体），全部约束来自最基本的加法和”不重复”原则。</p>

<p>Sum Table 揭示了它底层的组合结构：127 个可能的线索类型，41 个 magic block，最多 12 种组合的对称热图——这些数字都蕴含在 9 个数字的集合里，完全由加法和排列决定。</p>

<p>一道好的 Kakuro 谜题，就是在这个组合结构里精心选取一条路径，使得每一步推导都严丝合缝，最终汇成唯一一个答案。</p>

<hr />

<h2 id="完整代码">完整代码</h2>

<p><a href="https://github.com/morefreeze/morefreeze.github.io/blob/master/code/kakuro.py">kakuro.py</a> 包含 <code class="language-plaintext highlighter-rouge">build_sum_table()</code>、<code class="language-plaintext highlighter-rouge">KakuroPuzzle</code> 类、<code class="language-plaintext highlighter-rouge">encode_kakuro()</code>、<code class="language-plaintext highlighter-rouge">solve_kakuro()</code> 以及教学示例和 mini kakuro 的 demo，依赖同目录下的 <code class="language-plaintext highlighter-rouge">dlx_colors.py</code>。</p>

<p>本系列的其他文章：</p>
<ul>
  <li><a href="/2026/03/dlx-colors.html">颜色项与贴纸：XCC 的 bug 修复</a></li>
  <li><a href="/2026/03/dlx-xcc.html">拼字谜题：用 XCC 自动出题</a></li>
  <li><a href="/2026/03/zebra-puzzle.html">斑马谜题：逻辑推理题的 XCC 解法</a></li>
  <li><a href="/2026/03/pentomino-tiling.html">五格骨牌平铺</a></li>
  <li><a href="/2026/04/pentomino-coloring.html">五格骨牌三着色</a></li>
</ul>]]></content><author><name>More Freeze</name></author><category term="algorithm" /><category term="combinatorics" /><category term="DLX" /><category term="算法" /><category term="组合数学" /><category term="TAOCP" /><category term="精确覆盖" /><category term="Kakuro" /><summary type="html"><![CDATA[从数字填字游戏 Kakuro 出发，探索组合数学 Sum Table、XCC 精确覆盖编码，以及 Knuth 五格骨牌 Kakuro 彩蛋]]></summary></entry><entry><title type="html">五格骨牌的三染色问题：2339 种铺法里有多少种能上色？</title><link href="https://blog.morefreeze.top/2026/04/pentomino-coloring.html" rel="alternate" type="text/html" title="五格骨牌的三染色问题：2339 种铺法里有多少种能上色？" /><published>2026-04-07T00:00:00+00:00</published><updated>2026-04-07T00:00:00+00:00</updated><id>https://blog.morefreeze.top/2026/04/pentomino-coloring</id><content type="html" xml:base="https://blog.morefreeze.top/2026/04/pentomino-coloring.html"><![CDATA[<h2 id="引言">引言</h2>

<p>用 12 块五格骨牌平铺 6×10 矩形共有 2339 种方法——但如果我要求相邻骨牌颜色不同，连角都不能碰，还剩几种？</p>

<p>这是《The Art of Computer Programming》Volume 4B 中的习题 7.2.2.1-277，难度评级 [25]（TAOCP 的评级范围 0–50，数字越大越难）。答案是：<strong>94 种</strong>，仅占约 4%。</p>

<!--more-->

<h2 id="问题定义">问题定义</h2>

<h3 id="什么是强三染色">什么是”强”三染色？</h3>

<p>普通图染色问题中，两个顶点相邻指的是它们之间有边相连。在五格骨牌的语境下，如果两块骨牌<strong>共享一条边</strong>，它们就是相邻的。</p>

<p>但”强”染色的要求更严格：<strong>两块骨牌不能共享边，也不能共享角点</strong>。用图论的语言来说，这是 8-连通邻接（Chebyshev 距离 ≤ 1，即两个点在 x、y 方向的距离都不超过 1），而不是 4-连通邻接。</p>

<p><img src="/images/pentomino-adjacency.svg" alt="普通染色与强染色对比" style="width:100%; max-width:520px;" /></p>

<h3 id="目标">目标</h3>

<p>用红、白、蓝三种颜色给 12 块五格骨牌着色，使得任何两块相同颜色的骨牌既不共享边，也不共享角。</p>

<p>我们需要枚举所有 2339 种 6×10 的平铺方案，然后逐一检查哪些满足强三染色条件。</p>

<h2 id="两阶段算法">两阶段算法</h2>

<h3 id="阶段一dlx-枚举所有铺法">阶段一：DLX 枚举所有铺法</h3>

<p>这个阶段基于<a href="/2026/03/pentomino-tiling.html">前文</a>实现的 Dancing Links 求解器，封装一个枚举所有铺法的函数：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">get_all_packings</span><span class="p">(</span><span class="n">width</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">6</span><span class="p">,</span> <span class="n">height</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">10</span><span class="p">):</span>
    <span class="s">"""枚举所有五格骨牌平铺方案"""</span>
    <span class="n">dlx</span> <span class="o">=</span> <span class="n">DancingLinks</span><span class="p">(</span><span class="n">width</span> <span class="o">*</span> <span class="n">height</span> <span class="o">+</span> <span class="nb">len</span><span class="p">(</span><span class="n">PENTOMINOES</span><span class="p">))</span>
    <span class="c1"># ... 构建精确覆盖矩阵 ...
</span>    <span class="n">packings</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">solution</span> <span class="ow">in</span> <span class="n">dlx</span><span class="p">.</span><span class="n">search</span><span class="p">():</span>
        <span class="n">packing</span> <span class="o">=</span> <span class="p">{}</span>
        <span class="k">for</span> <span class="n">node</span> <span class="ow">in</span> <span class="n">solution</span><span class="p">:</span>
            <span class="c1"># 提取骨牌位置信息
</span>            <span class="n">name</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">orientation</span> <span class="o">=</span> <span class="n">row_info</span><span class="p">[</span><span class="n">node</span><span class="p">]</span>
            <span class="n">packing</span><span class="p">[</span><span class="n">name</span><span class="p">]</span> <span class="o">=</span> <span class="n">set_of_cells</span>
        <span class="n">packings</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">packing</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">packings</span>
</code></pre></div></div>

<p>这个函数会返回所有 9356 种平铺方案（包含板对称性），去重后是 2339 种。</p>

<h3 id="阶段二构建邻接图">阶段二：构建邻接图</h3>

<p>对于每一种平铺方案，我们需要构建一个邻接图，图中的顶点代表 12 块骨牌，边代表两块骨牌在 Chebyshev 距离 ≤ 1 的范围内。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">build_adjacency</span><span class="p">(</span><span class="n">packing</span><span class="p">):</span>
    <span class="s">"""构建强邻接图：两块骨牌相邻当且仅当它们的任意方格的 Chebyshev 距离 ≤ 1"""</span>
    <span class="n">names</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="n">packing</span><span class="p">.</span><span class="n">keys</span><span class="p">())</span>
    <span class="n">adjacency</span> <span class="o">=</span> <span class="p">{</span><span class="n">name</span><span class="p">:</span> <span class="nb">set</span><span class="p">()</span> <span class="k">for</span> <span class="n">name</span> <span class="ow">in</span> <span class="n">names</span><span class="p">}</span>

    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">names</span><span class="p">)):</span>
        <span class="k">for</span> <span class="n">j</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">i</span> <span class="o">+</span> <span class="mi">1</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">names</span><span class="p">)):</span>
            <span class="n">a</span><span class="p">,</span> <span class="n">b</span> <span class="o">=</span> <span class="n">names</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="n">names</span><span class="p">[</span><span class="n">j</span><span class="p">]</span>
            <span class="n">adjacent</span> <span class="o">=</span> <span class="bp">False</span>
            <span class="k">for</span> <span class="n">ax</span><span class="p">,</span> <span class="n">ay</span> <span class="ow">in</span> <span class="n">packing</span><span class="p">[</span><span class="n">a</span><span class="p">]:</span>
                <span class="k">for</span> <span class="n">bx</span><span class="p">,</span> <span class="n">by</span> <span class="ow">in</span> <span class="n">packing</span><span class="p">[</span><span class="n">b</span><span class="p">]:</span>
                    <span class="k">if</span> <span class="nb">abs</span><span class="p">(</span><span class="n">ax</span> <span class="o">-</span> <span class="n">bx</span><span class="p">)</span> <span class="o">&lt;=</span> <span class="mi">1</span> <span class="ow">and</span> <span class="nb">abs</span><span class="p">(</span><span class="n">ay</span> <span class="o">-</span> <span class="n">by</span><span class="p">)</span> <span class="o">&lt;=</span> <span class="mi">1</span><span class="p">:</span>
                        <span class="n">adjacent</span> <span class="o">=</span> <span class="bp">True</span>
                        <span class="k">break</span>
                <span class="k">if</span> <span class="n">adjacent</span><span class="p">:</span>
                    <span class="k">break</span>
            <span class="k">if</span> <span class="n">adjacent</span><span class="p">:</span>
                <span class="n">adjacency</span><span class="p">[</span><span class="n">a</span><span class="p">].</span><span class="n">add</span><span class="p">(</span><span class="n">b</span><span class="p">)</span>
                <span class="n">adjacency</span><span class="p">[</span><span class="n">b</span><span class="p">].</span><span class="n">add</span><span class="p">(</span><span class="n">a</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">adjacency</span>
</code></pre></div></div>

<p>这个邻接图是一个<strong>无向图</strong>，最多有 \(\binom{12}{2} = 66\) 条边（完全图的上界）。实际上由于骨牌形状和空间的限制，边数通常少于这个上界。</p>

<h3 id="阶段三回溯检验三染色性">阶段三：回溯检验三染色性</h3>

<p>有了邻接图，我们需要检验它是否可以 3-染色。这又是另一个经典的 NP 完全问题，但由于图很小（只有 12 个顶点），简单的回溯就足够了：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">is_three_colorable</span><span class="p">(</span><span class="n">adjacency</span><span class="p">):</span>
    <span class="s">"""通过回溯检验邻接图是否可以 3-染色"""</span>
    <span class="n">names</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="n">adjacency</span><span class="p">.</span><span class="n">keys</span><span class="p">())</span>
    <span class="n">n</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">names</span><span class="p">)</span>
    <span class="n">coloring</span> <span class="o">=</span> <span class="p">{}</span>

    <span class="k">def</span> <span class="nf">backtrack</span><span class="p">(</span><span class="n">idx</span><span class="p">):</span>
        <span class="k">if</span> <span class="n">idx</span> <span class="o">==</span> <span class="n">n</span><span class="p">:</span>
            <span class="k">return</span> <span class="bp">True</span>
        <span class="n">name</span> <span class="o">=</span> <span class="n">names</span><span class="p">[</span><span class="n">idx</span><span class="p">]</span>
        <span class="c1"># 找出所有已染色的邻居的颜色
</span>        <span class="n">used</span> <span class="o">=</span> <span class="p">{</span><span class="n">coloring</span><span class="p">[</span><span class="n">nb</span><span class="p">]</span> <span class="k">for</span> <span class="n">nb</span> <span class="ow">in</span> <span class="n">adjacency</span><span class="p">[</span><span class="n">name</span><span class="p">]</span> <span class="k">if</span> <span class="n">nb</span> <span class="ow">in</span> <span class="n">coloring</span><span class="p">}</span>
        <span class="c1"># 尝试三种颜色
</span>        <span class="k">for</span> <span class="n">color</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">3</span><span class="p">):</span>
            <span class="k">if</span> <span class="n">color</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">used</span><span class="p">:</span>
                <span class="n">coloring</span><span class="p">[</span><span class="n">name</span><span class="p">]</span> <span class="o">=</span> <span class="n">color</span>
                <span class="k">if</span> <span class="n">backtrack</span><span class="p">(</span><span class="n">idx</span> <span class="o">+</span> <span class="mi">1</span><span class="p">):</span>
                    <span class="k">return</span> <span class="bp">True</span>
                <span class="k">del</span> <span class="n">coloring</span><span class="p">[</span><span class="n">name</span><span class="p">]</span>
        <span class="k">return</span> <span class="bp">False</span>

    <span class="k">if</span> <span class="n">backtrack</span><span class="p">(</span><span class="mi">0</span><span class="p">):</span>
        <span class="k">return</span> <span class="bp">True</span><span class="p">,</span> <span class="n">coloring</span>
    <span class="k">return</span> <span class="bp">False</span><span class="p">,</span> <span class="bp">None</span>
</code></pre></div></div>

<p>这里的优化在于：我们只考虑已染色邻居的颜色，这样可以尽早剪枝。</p>

<h2 id="结果与分析">结果与分析</h2>

<h3 id="统计结果">统计结果</h3>

<p>运行完整程序后，我们得到：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Total packings (with sym):  9356
3-colorable (with sym):     376
Non-3-colorable (with sym): 8980

Distinct packings:          2339
Distinct 3-colorable:       94
Distinct non-3-colorable:   2245
</code></pre></div></div>

<p>只有 <strong>94/2339 ≈ 4.0%</strong> 的平铺方案满足强三染色条件。这个比例之低说明了”强”条件的苛刻性。</p>

<h3 id="示例染色">示例染色</h3>

<p>下面是一个满足强三染色条件的平铺方案：</p>

<p><img src="/images/pentomino-3coloring.svg" alt="强三染色示例" style="width:100%; max-width:480px;" /></p>

<p>你可以验证：任意两块相同颜色的骨牌既不共享边，也不共享角。</p>

<h3 id="为什么这么少">为什么这么少？</h3>

<p>让我们分析一下为什么强三染色条件如此苛刻：</p>

<ol>
  <li>
    <p><strong>邻接图密度更高</strong>：在强染色条件下，邻接图的边数大大增加。原本不共享边的两块骨牌可能因为共享角点而变为相邻。</p>
  </li>
  <li>
    <p><strong>空间约束</strong>：12 块骨牌挤在 60 个格子里，平均每块骨牌有 5 个格子。在 Chebyshev 距离 ≤ 1 的定义下，每块骨牌的”影响范围”更大，更容易与其他骨牌冲突。</p>
  </li>
  <li>
    <p><strong>颜色的均匀性</strong>：三种颜色要分配给 12 块骨牌，理想情况下每种颜色 4 块。但由于邻接图的稠密性，很难找到这样的完美分配。</p>
  </li>
</ol>

<h2 id="算法复杂度分析">算法复杂度分析</h2>

<h3 id="时间复杂度">时间复杂度</h3>

<ul>
  <li>
    <p><strong>阶段一</strong>：DLX 枚举所有平铺方案。6×10 的情况有 9356 种方案（含对称性），DLX 的实际运行时间在秒级。</p>
  </li>
  <li>
    <p><strong>阶段二</strong>：对于每种方案构建邻接图，需要检查 \(\binom{12}{2} \times 5^2 = 1650\) 次方格对（上界为 \(12^2 \times 25 = 3600\)）。</p>
  </li>
  <li>
    <p><strong>阶段三</strong>：回溯检验三染色性。最坏情况下是 \(O(3^{12})\)，但由于邻接约束和剪枝，实际运行时间非常快。</p>
  </li>
</ul>

<p>总体的实际运行时间在几分钟级别，主要时间花在 DLX 枚举上。</p>

<h3 id="空间复杂度">空间复杂度</h3>

<ul>
  <li>存储 9356 种平铺方案，每种方案包含 12 块骨牌的位置信息。</li>
  <li>邻接图和染色方案的临时存储。</li>
</ul>

<p>空间需求在百 MB 级别，现代计算机完全可以处理。</p>

<h2 id="扩展思考">扩展思考</h2>

<h3 id="更一般的染色问题">更一般的染色问题</h3>

<p>我们可以把这个问题推广：</p>

<ul>
  <li>
    <p><strong>k-染色</strong>：用 k 种颜色给骨牌着色。直觉上，k 越大可行性越高；当 k = 2 时，几乎不可行。</p>
  </li>
  <li>
    <p><strong>不同板形</strong>：5×12、4×15、3×20 等其他矩形板的三染色性如何？</p>
  </li>
  <li>
    <p><strong>不同骨牌集</strong>：如果不用全部 12 种骨牌，或者允许重复使用某些骨牌，情况会如何？</p>
  </li>
</ul>

<h3 id="与其他问题的联系">与其他问题的联系</h3>

<p>这个问题与以下几个领域有深刻联系：</p>

<ol>
  <li>
    <p><strong>图染色理论</strong>：经典的 NP 完全问题，但在小规模实例上可以通过回溯解决。</p>
  </li>
  <li>
    <p><strong>精确覆盖问题</strong>：DLX 算法的应用之一，展现了算法设计的优雅性。</p>
  </li>
  <li>
    <p><strong>组合设计</strong>：如何在有限空间中安排形状，满足特定的约束条件。</p>
  </li>
</ol>

<h2 id="总结">总结</h2>

<p>通过这个问题，我们看到了：</p>

<ul>
  <li>
    <p><strong>DLX 的威力</strong>：不仅解决单个实例，还能枚举所有解，为后续分析提供基础。</p>
  </li>
  <li>
    <p><strong>回溯的适用场景</strong>：对于小规模的 NP 完全问题，回溯配合剪枝完全实用。</p>
  </li>
  <li>
    <p><strong>约束的影响</strong>：从”边相邻”到”边或角相邻”，看似微小的变化，却使得可行解从”几乎所有”变成”极少数”。</p>
  </li>
</ul>

<p>完整的代码实现可以在 <a href="https://github.com/morefreeze/morefreeze.github.io/blob/master/code/pentomino_coloring.py">GitHub</a> 上找到。如果你对 DLX 算法感兴趣，可以参考<a href="/2026/03/pentomino-tiling.html">前文</a>了解更多细节。</p>

<h2 id="参考资料">参考资料</h2>

<ul>
  <li>Donald Knuth, <em>The Art of Computer Programming</em>, Volume 4B, Section 7.2.2.1</li>
  <li>Dancing Links 算法原始论文：<a href="https://arxiv.org/abs/cs/0011047">Dancing Links</a></li>
  <li>五格骨牌平铺问题的 Wikipedia：<a href="https://en.wikipedia.org/wiki/Pentomino">Pentomino</a></li>
</ul>]]></content><author><name>More Freeze</name></author><category term="algorithm" /><category term="combinatorics" /><category term="DLX" /><category term="算法" /><category term="组合数学" /><category term="TAOCP" /><category term="精确覆盖" /><category term="图染色" /><category term="五格骨牌" /><summary type="html"><![CDATA[TAOCP 习题 277：枚举 6×10 五格骨牌铺法，检验强三染色性——相邻骨牌连角都不能碰]]></summary></entry><entry><title type="html">Strong 3-Coloring Pentomino Tilings: Only 94 Out of 2339 Survive</title><link href="https://blog.morefreeze.top/2026/04/pentomino-coloring_en.html" rel="alternate" type="text/html" title="Strong 3-Coloring Pentomino Tilings: Only 94 Out of 2339 Survive" /><published>2026-04-07T00:00:00+00:00</published><updated>2026-04-07T00:00:00+00:00</updated><id>https://blog.morefreeze.top/2026/04/pentomino-coloring_en</id><content type="html" xml:base="https://blog.morefreeze.top/2026/04/pentomino-coloring_en.html"><![CDATA[<h2 id="introduction">Introduction</h2>

<p>There are 2,339 ways to tile a 6×10 rectangle with the 12 pentominoes. But what if I add a twist: adjacent pieces must receive different colors, and even corner-touching doesn’t count as “far enough”? How many survive?</p>

<p>This is Exercise 7.2.2.1-277 from <em>The Art of Computer Programming</em>, Volume 4B — difficulty rating [25] on Knuth’s 0–50 scale. The answer: <strong>94</strong>, a mere 4%.</p>

<!--more-->

<h2 id="problem-definition">Problem Definition</h2>

<h3 id="what-makes-it-strong">What Makes It “Strong”?</h3>

<p>In ordinary graph coloring, two vertices are adjacent only if they share an edge. For pentomino tiling, two pieces are adjacent when they share at least one grid edge.</p>

<p>“Strong” coloring is stricter: <strong>two pieces may not share an edge <em>or</em> a corner</strong>. In graph-theoretic terms, this is 8-connectivity (Chebyshev distance ≤ 1), rather than 4-connectivity.</p>

<p><img src="/images/pentomino-adjacency.svg" alt="4-connectivity vs 8-connectivity comparison" style="width:100%; max-width:560px;" /></p>

<h3 id="goal">Goal</h3>

<p>Color all 12 pentominoes with red, white, and blue so that no two same-colored pieces share an edge or a corner. We need to enumerate all 2,339 packings of the 6×10 board and check each one for strong 3-colorability.</p>

<h2 id="two-phase-algorithm">Two-Phase Algorithm</h2>

<h3 id="phase-1-dlx-enumeration">Phase 1: DLX Enumeration</h3>

<p>This phase builds on the <a href="/2026/03/pentomino-tiling.html">Dancing Links solver</a> from the previous post, wrapping it into a function that enumerates all packings:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">get_all_packings</span><span class="p">(</span><span class="n">width</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">6</span><span class="p">,</span> <span class="n">height</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">10</span><span class="p">):</span>
    <span class="s">"""Enumerate all pentomino tiling solutions"""</span>
    <span class="n">dlx</span> <span class="o">=</span> <span class="n">DancingLinks</span><span class="p">(</span><span class="n">width</span> <span class="o">*</span> <span class="n">height</span> <span class="o">+</span> <span class="nb">len</span><span class="p">(</span><span class="n">PENTOMINOES</span><span class="p">))</span>
    <span class="c1"># ... build exact cover matrix ...
</span>    <span class="n">packings</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">solution</span> <span class="ow">in</span> <span class="n">dlx</span><span class="p">.</span><span class="n">search</span><span class="p">():</span>
        <span class="n">packing</span> <span class="o">=</span> <span class="p">{}</span>
        <span class="k">for</span> <span class="n">node</span> <span class="ow">in</span> <span class="n">solution</span><span class="p">:</span>
            <span class="n">name</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">orientation</span> <span class="o">=</span> <span class="n">row_info</span><span class="p">[</span><span class="n">node</span><span class="p">]</span>
            <span class="n">packing</span><span class="p">[</span><span class="n">name</span><span class="p">]</span> <span class="o">=</span> <span class="n">set_of_cells</span>
        <span class="n">packings</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">packing</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">packings</span>
</code></pre></div></div>

<p>This returns 9,356 solutions including board symmetries; after deduplication, we get 2,339 distinct packings.</p>

<h3 id="phase-2-building-the-adjacency-graph">Phase 2: Building the Adjacency Graph</h3>

<p>For each packing, we build an adjacency graph where vertices represent the 12 pieces and edges connect pieces within Chebyshev distance ≤ 1:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">build_adjacency</span><span class="p">(</span><span class="n">packing</span><span class="p">):</span>
    <span class="s">"""Build strong adjacency graph: two pieces adjacent iff any of their cells are within Chebyshev distance 1"""</span>
    <span class="n">names</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="n">packing</span><span class="p">.</span><span class="n">keys</span><span class="p">())</span>
    <span class="n">adjacency</span> <span class="o">=</span> <span class="p">{</span><span class="n">name</span><span class="p">:</span> <span class="nb">set</span><span class="p">()</span> <span class="k">for</span> <span class="n">name</span> <span class="ow">in</span> <span class="n">names</span><span class="p">}</span>

    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">names</span><span class="p">)):</span>
        <span class="k">for</span> <span class="n">j</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">i</span> <span class="o">+</span> <span class="mi">1</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">names</span><span class="p">)):</span>
            <span class="n">a</span><span class="p">,</span> <span class="n">b</span> <span class="o">=</span> <span class="n">names</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="n">names</span><span class="p">[</span><span class="n">j</span><span class="p">]</span>
            <span class="n">adjacent</span> <span class="o">=</span> <span class="bp">False</span>
            <span class="k">for</span> <span class="n">ax</span><span class="p">,</span> <span class="n">ay</span> <span class="ow">in</span> <span class="n">packing</span><span class="p">[</span><span class="n">a</span><span class="p">]:</span>
                <span class="k">for</span> <span class="n">bx</span><span class="p">,</span> <span class="n">by</span> <span class="ow">in</span> <span class="n">packing</span><span class="p">[</span><span class="n">b</span><span class="p">]:</span>
                    <span class="k">if</span> <span class="nb">abs</span><span class="p">(</span><span class="n">ax</span> <span class="o">-</span> <span class="n">bx</span><span class="p">)</span> <span class="o">&lt;=</span> <span class="mi">1</span> <span class="ow">and</span> <span class="nb">abs</span><span class="p">(</span><span class="n">ay</span> <span class="o">-</span> <span class="n">by</span><span class="p">)</span> <span class="o">&lt;=</span> <span class="mi">1</span><span class="p">:</span>
                        <span class="n">adjacent</span> <span class="o">=</span> <span class="bp">True</span>
                        <span class="k">break</span>
                <span class="k">if</span> <span class="n">adjacent</span><span class="p">:</span>
                    <span class="k">break</span>
            <span class="k">if</span> <span class="n">adjacent</span><span class="p">:</span>
                <span class="n">adjacency</span><span class="p">[</span><span class="n">a</span><span class="p">].</span><span class="n">add</span><span class="p">(</span><span class="n">b</span><span class="p">)</span>
                <span class="n">adjacency</span><span class="p">[</span><span class="n">b</span><span class="p">].</span><span class="n">add</span><span class="p">(</span><span class="n">a</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">adjacency</span>
</code></pre></div></div>

<p>This undirected graph has at most \(\binom{12}{2} = 66\) edges. In practice, the count is lower due to spatial constraints.</p>

<h3 id="phase-3-backtracking-3-colorability-test">Phase 3: Backtracking 3-Colorability Test</h3>

<p>Testing 3-colorability is another NP-complete problem, but the graph is tiny (12 vertices). Simple backtracking suffices:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">is_three_colorable</span><span class="p">(</span><span class="n">adjacency</span><span class="p">):</span>
    <span class="s">"""Check 3-colorability via backtracking"""</span>
    <span class="n">names</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="n">adjacency</span><span class="p">.</span><span class="n">keys</span><span class="p">())</span>
    <span class="n">n</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">names</span><span class="p">)</span>
    <span class="n">coloring</span> <span class="o">=</span> <span class="p">{}</span>

    <span class="k">def</span> <span class="nf">backtrack</span><span class="p">(</span><span class="n">idx</span><span class="p">):</span>
        <span class="k">if</span> <span class="n">idx</span> <span class="o">==</span> <span class="n">n</span><span class="p">:</span>
            <span class="k">return</span> <span class="bp">True</span>
        <span class="n">name</span> <span class="o">=</span> <span class="n">names</span><span class="p">[</span><span class="n">idx</span><span class="p">]</span>
        <span class="n">used</span> <span class="o">=</span> <span class="p">{</span><span class="n">coloring</span><span class="p">[</span><span class="n">nb</span><span class="p">]</span> <span class="k">for</span> <span class="n">nb</span> <span class="ow">in</span> <span class="n">adjacency</span><span class="p">[</span><span class="n">name</span><span class="p">]</span> <span class="k">if</span> <span class="n">nb</span> <span class="ow">in</span> <span class="n">coloring</span><span class="p">}</span>
        <span class="k">for</span> <span class="n">color</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">3</span><span class="p">):</span>
            <span class="k">if</span> <span class="n">color</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">used</span><span class="p">:</span>
                <span class="n">coloring</span><span class="p">[</span><span class="n">name</span><span class="p">]</span> <span class="o">=</span> <span class="n">color</span>
                <span class="k">if</span> <span class="n">backtrack</span><span class="p">(</span><span class="n">idx</span> <span class="o">+</span> <span class="mi">1</span><span class="p">):</span>
                    <span class="k">return</span> <span class="bp">True</span>
                <span class="k">del</span> <span class="n">coloring</span><span class="p">[</span><span class="n">name</span><span class="p">]</span>
        <span class="k">return</span> <span class="bp">False</span>

    <span class="k">if</span> <span class="n">backtrack</span><span class="p">(</span><span class="mi">0</span><span class="p">):</span>
        <span class="k">return</span> <span class="bp">True</span><span class="p">,</span> <span class="n">coloring</span>
    <span class="k">return</span> <span class="bp">False</span><span class="p">,</span> <span class="bp">None</span>
</code></pre></div></div>

<p>The key optimization: only consider colors already assigned to neighbors, enabling early pruning.</p>

<h2 id="results">Results</h2>

<h3 id="statistics">Statistics</h3>

<p>Running the full program produces:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Total packings (with sym):  9356
3-colorable (with sym):     376
Non-3-colorable (with sym): 8980

Distinct packings:          2339
Distinct 3-colorable:       94
Distinct non-3-colorable:   2245
</code></pre></div></div>

<p>Only <strong>94/2339 ≈ 4.0%</strong> of packings satisfy the strong 3-coloring condition — a striking illustration of how restrictive the “strong” requirement is.</p>

<h3 id="example-coloring">Example Coloring</h3>

<p>Here is one valid strong 3-coloring:</p>

<p><img src="/images/pentomino-3coloring.svg" alt="Strong 3-coloring example" style="width:100%; max-width:480px;" /></p>

<p>You can verify: no two same-colored pieces share an edge or a corner.</p>

<h3 id="why-so-few">Why So Few?</h3>

<p>Three factors conspire to eliminate nearly all packings:</p>

<ol>
  <li>
    <p><strong>Denser adjacency graph</strong>: Corner-touching adds many edges that ordinary edge-adjacency misses.</p>
  </li>
  <li>
    <p><strong>Spatial constraints</strong>: 12 pieces in 60 cells means each piece occupies only 5 cells. Under Chebyshev distance ≤ 1, every piece’s “influence zone” is larger, causing more conflicts.</p>
  </li>
  <li>
    <p><strong>Color balance</strong>: Three colors for 12 pieces ideally means 4 per color. The graph’s density makes such an even split hard to achieve.</p>
  </li>
</ol>

<h2 id="complexity-analysis">Complexity Analysis</h2>

<h3 id="time">Time</h3>

<ul>
  <li><strong>Phase 1</strong>: DLX enumerates 9,356 solutions (with symmetries) in seconds.</li>
  <li><strong>Phase 2</strong>: Per packing, checks \(\binom{12}{2} \times 5^2 = 1{,}650\) cell pairs (upper bound \(12^2 \times 25 = 3{,}600\)).</li>
  <li><strong>Phase 3</strong>: Worst case \(O(3^{12})\), but adjacency constraints prune aggressively.</li>
</ul>

<p>Total wall-clock time: a few minutes, dominated by DLX enumeration.</p>

<h3 id="space">Space</h3>

<p>Storing 9,356 packings with position data for 12 pieces each — a few hundred MB, well within modern hardware.</p>

<h2 id="extensions">Extensions</h2>

<ul>
  <li><strong>k-coloring</strong>: Higher k means more feasible solutions; k = 2 is almost certainly impossible.</li>
  <li><strong>Other boards</strong>: How do 5×12, 4×15, 3×20 fare under strong 3-coloring?</li>
  <li><strong>Partial piece sets</strong>: What if we drop or duplicate certain pentominoes?</li>
</ul>

<h2 id="conclusion">Conclusion</h2>

<ul>
  <li><strong>DLX’s enumerative power</strong>: Not just a solver — a complete enumeration engine that enables downstream analysis.</li>
  <li><strong>Backtracking for small NP-complete instances</strong>: With pruning, even exponential algorithms run fast on tiny graphs.</li>
  <li><strong>The power of constraints</strong>: Switching from “edge-adjacent” to “edge-or-corner-adjacent” — a seemingly minor change — collapses the feasible set from “nearly everything” to “almost nothing.”</li>
</ul>

<p>The complete implementation is available on <a href="https://github.com/morefreeze/morefreeze.github.io/blob/master/code/pentomino_coloring.py">GitHub</a>. For more on the DLX algorithm itself, see the <a href="/2026/03/pentomino-tiling.html">previous post</a>.</p>

<h2 id="references">References</h2>

<ul>
  <li>Donald Knuth, <em>The Art of Computer Programming</em>, Volume 4B, Section 7.2.2.1</li>
  <li>Dancing Links original paper: <a href="https://arxiv.org/abs/cs/0011047">Dancing Links</a></li>
  <li>Pentomino tiling on Wikipedia: <a href="https://en.wikipedia.org/wiki/Pentomino">Pentomino</a></li>
</ul>]]></content><author><name>More Freeze</name></author><category term="algorithm" /><category term="combinatorics" /><category term="DLX" /><category term="algorithm" /><category term="combinatorics" /><category term="TAOCP" /><category term="exact-cover" /><category term="graph-coloring" /><category term="pentomino" /><summary type="html"><![CDATA[TAOCP Exercise 7.2.2.1-277: enumerate all 2339 pentomino packings of a 6x10 board, then test which ones admit a strong 3-coloring where even corner-touching pieces must differ]]></summary></entry><entry><title type="html">用 Dancing Links 解决五格骨牌平铺问题</title><link href="https://blog.morefreeze.top/2026/03/pentomino-tiling.html" rel="alternate" type="text/html" title="用 Dancing Links 解决五格骨牌平铺问题" /><published>2026-03-31T00:00:00+00:00</published><updated>2026-03-31T00:00:00+00:00</updated><id>https://blog.morefreeze.top/2026/03/pentomino-tiling</id><content type="html" xml:base="https://blog.morefreeze.top/2026/03/pentomino-tiling.html"><![CDATA[<h2 id="引言">引言</h2>

<p>Donald Knuth 在《The Art of Computer Programming》Volume 4B 中提出了 Dancing Links (DLX) 算法——一种优雅地解决精确覆盖问题的回溯算法。本文将探讨习题 7.2.2.1-31 中提到的经典应用：<strong>五格骨牌平铺问题</strong>。</p>

<h3 id="什么是五格骨牌">什么是五格骨牌？</h3>

<p>五格骨牌（Pentominoes）是由 5 个单位正方形连接而成的多格骨牌。不考虑旋转和翻转，共有 <strong>12 种</strong> 不同的五格骨牌，通常用字母 F, I, L, P, N, T, U, V, W, X, Y, Z 来命名：</p>

<p><img src="/images/pentominoes.svg" alt="12种五格骨牌形状" style="width:100%; max-width:840px;" /></p>

<h3 id="问题定义">问题定义</h3>

<p><strong>经典问题</strong>：用全部 12 种五格骨牌各一片，能否精确平铺一个 6×10 的矩形区域？</p>

<p>这个问题可以推广到：</p>
<ul>
  <li>5×12 矩形</li>
  <li>4×15 矩形</li>
  <li>3×20 矩形</li>
</ul>

<!--more-->

<h2 id="转化为精确覆盖问题">转化为精确覆盖问题</h2>

<p>Dancing Links 算法解决的是<strong>精确覆盖问题</strong>：</p>

<blockquote>
  <p>给定一个 0-1 矩阵，选择若干行，使得每一列恰好有一个 1。</p>
</blockquote>

<h3 id="构建矩阵">构建矩阵</h3>

<p>对于五格骨牌平铺问题，我们构造如下矩阵：</p>

<p><strong>列的构造</strong>：</p>
<ul>
  <li>每个网格位置（6×10 = 60 列）</li>
  <li>每种五格骨牌（12 列）</li>
  <li>总共 72 列</li>
</ul>

<p><strong>行的构造</strong>：</p>
<ul>
  <li>对于每种五格骨牌</li>
  <li>考虑其在矩形内的所有合法位置和方向</li>
  <li>每行对应一个”放置选项”</li>
  <li>该行的 1 标记被占用的网格和使用的骨牌类型</li>
</ul>

<h3 id="举例说明">举例说明</h3>

<p>假设我们要放置 <strong>L 型五格骨牌</strong>在位置 (0,0)：</p>

<p><img src="/images/pentomino-L-example.svg" alt="L型五格骨牌放置示例" style="width:100%; max-width:300px;" /></p>

<p>行表示：<code class="language-plaintext highlighter-rouge">[L型, (0,0), (1,0), (2,0), (3,0), (3,1)]</code>，对应 6 个 1。</p>

<h2 id="dancing-links-算法">Dancing Links 算法</h2>

<p>DLX 的核心思想是用<strong>双向十字链表</strong>表示稀疏矩阵，实现高效的回溯：</p>

<h3 id="数据结构">数据结构</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ColumnHeader ←→ ColumnHeader ←→ ColumnHeader ←→ ...
       ↓                ↓               ↓
    DataNode ↔ DataNode ↔ DataNode
       ↓
    DataNode
</code></pre></div></div>

<h3 id="算法流程">算法流程</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Algorithm X(cover):
    if matrix empty:
        solution found!
    choose column c (fewest options)
    cover column c
    for each row r in column c:
        add r to solution
        for each column j in row r:
            cover column j
        X(cover)
        remove r from solution
        for each column j in row r:
            uncover column j
    uncover column c
</code></pre></div></div>

<h3 id="优化选择最少选项的列">优化：选择最少选项的列</h3>

<p>这是 DLX 的关键优化——<strong>总是选择约束最紧的列</strong>，这样能最大限度地剪枝。</p>

<h2 id="核心实现">核心实现</h2>

<p>DLX 的核心在于 <code class="language-plaintext highlighter-rouge">cover</code>（覆盖）和 <code class="language-plaintext highlighter-rouge">uncover</code>（撤销）操作，它们利用双向链表实现高效的回溯：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">cover</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">col</span><span class="p">):</span>
    <span class="s">"""覆盖列 col：从矩阵中移除该列及所有相关行"""</span>
    <span class="c1"># 1. 从列链表中移除该列
</span>    <span class="bp">self</span><span class="p">.</span><span class="n">R</span><span class="p">[</span><span class="bp">self</span><span class="p">.</span><span class="n">L</span><span class="p">[</span><span class="n">col</span><span class="p">]]</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">R</span><span class="p">[</span><span class="n">col</span><span class="p">]</span>
    <span class="bp">self</span><span class="p">.</span><span class="n">L</span><span class="p">[</span><span class="bp">self</span><span class="p">.</span><span class="n">R</span><span class="p">[</span><span class="n">col</span><span class="p">]]</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">L</span><span class="p">[</span><span class="n">col</span><span class="p">]</span>

    <span class="c1"># 2. 移除所有与该列相交的行
</span>    <span class="n">i</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">D</span><span class="p">[</span><span class="n">col</span><span class="p">]</span>
    <span class="k">while</span> <span class="n">i</span> <span class="o">!=</span> <span class="n">col</span><span class="p">:</span>
        <span class="n">j</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">R</span><span class="p">[</span><span class="n">i</span><span class="p">]</span>
        <span class="k">while</span> <span class="n">j</span> <span class="o">!=</span> <span class="n">i</span><span class="p">:</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">D</span><span class="p">[</span><span class="bp">self</span><span class="p">.</span><span class="n">U</span><span class="p">[</span><span class="n">j</span><span class="p">]]</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">D</span><span class="p">[</span><span class="n">j</span><span class="p">]</span>  <span class="c1"># 跳过节点 j
</span>            <span class="bp">self</span><span class="p">.</span><span class="n">U</span><span class="p">[</span><span class="bp">self</span><span class="p">.</span><span class="n">D</span><span class="p">[</span><span class="n">j</span><span class="p">]]</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">U</span><span class="p">[</span><span class="n">j</span><span class="p">]</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">S</span><span class="p">[</span><span class="bp">self</span><span class="p">.</span><span class="n">C</span><span class="p">[</span><span class="n">j</span><span class="p">]]</span> <span class="o">-=</span> <span class="mi">1</span>  <span class="c1"># 减少列计数
</span>            <span class="n">j</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">R</span><span class="p">[</span><span class="n">j</span><span class="p">]</span>
        <span class="n">i</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">D</span><span class="p">[</span><span class="n">i</span><span class="p">]</span>

<span class="k">def</span> <span class="nf">uncover</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">col</span><span class="p">):</span>
    <span class="s">"""撤销覆盖：精确逆向执行 cover 操作"""</span>
    <span class="c1"># 1. 恢复所有被移除的行（逆序）
</span>    <span class="n">i</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">U</span><span class="p">[</span><span class="n">col</span><span class="p">]</span>
    <span class="k">while</span> <span class="n">i</span> <span class="o">!=</span> <span class="n">col</span><span class="p">:</span>
        <span class="n">j</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">L</span><span class="p">[</span><span class="n">i</span><span class="p">]</span>
        <span class="k">while</span> <span class="n">j</span> <span class="o">!=</span> <span class="n">i</span><span class="p">:</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">S</span><span class="p">[</span><span class="bp">self</span><span class="p">.</span><span class="n">C</span><span class="p">[</span><span class="n">j</span><span class="p">]]</span> <span class="o">+=</span> <span class="mi">1</span>  <span class="c1"># 恢复列计数
</span>            <span class="bp">self</span><span class="p">.</span><span class="n">D</span><span class="p">[</span><span class="bp">self</span><span class="p">.</span><span class="n">U</span><span class="p">[</span><span class="n">j</span><span class="p">]]</span> <span class="o">=</span> <span class="n">j</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">U</span><span class="p">[</span><span class="bp">self</span><span class="p">.</span><span class="n">D</span><span class="p">[</span><span class="n">j</span><span class="p">]]</span> <span class="o">=</span> <span class="n">j</span>
            <span class="n">j</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">L</span><span class="p">[</span><span class="n">j</span><span class="p">]</span>
        <span class="n">i</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">U</span><span class="p">[</span><span class="n">i</span><span class="p">]</span>

    <span class="c1"># 2. 恢复列到列链表
</span>    <span class="bp">self</span><span class="p">.</span><span class="n">R</span><span class="p">[</span><span class="bp">self</span><span class="p">.</span><span class="n">L</span><span class="p">[</span><span class="n">col</span><span class="p">]]</span> <span class="o">=</span> <span class="n">col</span>
    <span class="bp">self</span><span class="p">.</span><span class="n">L</span><span class="p">[</span><span class="bp">self</span><span class="p">.</span><span class="n">R</span><span class="p">[</span><span class="n">col</span><span class="p">]]</span> <span class="o">=</span> <span class="n">col</span>
</code></pre></div></div>

<p><strong>关键点</strong>：</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">cover</code> 操作必须是<strong>可逆的</strong>，以便回溯时能精确恢复状态</li>
  <li>使用 <code class="language-plaintext highlighter-rouge">L</code> 和 <code class="language-plaintext highlighter-rouge">R</code> 指针在水平方向跳过节点</li>
  <li>使用 <code class="language-plaintext highlighter-rouge">U</code> 和 <code class="language-plaintext highlighter-rouge">D</code> 指针在垂直方向跳过节点</li>
  <li><code class="language-plaintext highlighter-rouge">S</code> 数组跟踪每列的节点数，用于 MRV 启发式</li>
</ul>

<p>完整代码实现见：<a href="https://github.com/morefreeze/morefreeze.github.io/blob/master/code/pentomino_dlx.py">GitHub - pentomino_dlx.py</a></p>

<h2 id="运行结果">运行结果</h2>

<p><img src="/images/pentomino-6x10-solution.svg" alt="6x10五格骨牌平铺解" style="width:100%; max-width:480px;" /></p>

<p><em>用全部 12 种五格骨牌平铺 6×10 矩形的一种解</em></p>

<h2 id="复杂度分析">复杂度分析</h2>

<h3 id="空间复杂度">空间复杂度</h3>

<ul>
  <li>对于 6×10 矩形，每列最多有 \(O(n)\) 个节点</li>
  <li>总节点数约为放置选项的数量</li>
  <li>空间复杂度：\(O(\text{放置选项})\)</li>
</ul>

<h3 id="时间复杂度">时间复杂度</h3>

<ul>
  <li>最坏情况是指数级：\(O(2^n)\)</li>
  <li>实际运行中，DLX 的剪枝效果显著</li>
  <li>对于五格骨牌问题，通常在毫秒级找到解</li>
</ul>

<h2 id="扩展与思考">扩展与思考</h2>

<h3 id="1-其他多格骨牌">1. 其他多格骨牌</h3>

<ul>
  <li><strong>三格骨牌</strong>：2 种（直线形、L形）</li>
  <li><strong>四格骨牌</strong>：5 种</li>
  <li><strong>六格骨牌</strong>：35 种——有趣的是，全部 35 种六格骨牌<strong>无法</strong>平铺任何矩形。棋盘着色可以证明：24 种”奇”骨牌覆盖 3 黑 3 白，11 种”偶”骨牌覆盖 4 黑 2 白（或 2 黑 4 白），总覆盖黑格数恒为偶数；而 10×21 等矩形有 105 格黑格（奇数），产生矛盾。但它们可以平铺带洞的区域（如 15×15 挖去中央 3×5）。完整代码见 <a href="https://github.com/morefreeze/morefreeze.github.io/blob/master/code/hexomino_dlx.py">hexomino_dlx.py</a>。</li>
</ul>

<h3 id="2-计数问题">2. 计数问题</h3>

<p>根据文献记载，五格骨牌平铺 6×10 矩形共有 <strong>9,356</strong> 种不同的解（考虑旋转和翻转）。这个数字是组合数学中的经典结果，由多种不同方法验证得出。</p>

<h3 id="3-变体问题">3. 变体问题</h3>

<ul>
  <li>有洞的矩形</li>
  <li>非矩形区域</li>
  <li>使用部分骨牌</li>
  <li>重复使用某些骨牌</li>
</ul>

<h2 id="总结">总结</h2>

<p>五格骨牌平铺问题完美展示了 Dancing Links 算法的威力：</p>

<ol>
  <li><strong>建模优雅</strong>：将几何问题转化为精确覆盖问题</li>
  <li><strong>算法高效</strong>：双向链表实现快速回溯</li>
  <li><strong>扩展性强</strong>：可轻松推广到其他约束满足问题</li>
</ol>

<p>这正是 Knuth 大师的设计哲学：<strong>找到问题的本质表示，然后用最恰当的数据结构来实现它</strong>。</p>

<h2 id="参考资料">参考资料</h2>

<ul>
  <li>Knuth, D. E. (2022). <em>The Art of Computer Programming, Volume 4B: Combinatorial Algorithms, Part 2</em>. Addison-Wesley.</li>
  <li><a href="https://en.wikipedia.org/wiki/Dancing_Links">Dancing Links - Wikipedia</a></li>
  <li><a href="https://en.wikipedia.org/wiki/Exact_cover">Exact Cover - Wikipedia</a></li>
</ul>

<hr />]]></content><author><name>More Freeze</name></author><category term="algorithm" /><category term="combinatorics" /><category term="DLX" /><category term="算法" /><category term="组合数学" /><category term="TAOCP" /><category term="精确覆盖" /><summary type="html"><![CDATA[探索 TAOCP 中五格骨牌平铺问题，学习如何用 Dancing Links 算法将平铺问题转化为精确覆盖问题]]></summary></entry><entry><title type="html">Pentomino Tiling with Dancing Links</title><link href="https://blog.morefreeze.top/2026/03/pentomino-tiling_en.html" rel="alternate" type="text/html" title="Pentomino Tiling with Dancing Links" /><published>2026-03-31T00:00:00+00:00</published><updated>2026-03-31T00:00:00+00:00</updated><id>https://blog.morefreeze.top/2026/03/pentomino-tiling_en</id><content type="html" xml:base="https://blog.morefreeze.top/2026/03/pentomino-tiling_en.html"><![CDATA[<h2 id="introduction">Introduction</h2>

<p>Donald Knuth introduced the Dancing Links (DLX) algorithm in <em>The Art of Computer Programming</em> Volume 4B — an elegant backtracking algorithm for solving exact cover problems. This post explores the classic application mentioned in Exercise 7.2.2.1-31: <strong>pentomino tiling</strong>.</p>

<h3 id="what-are-pentominoes">What Are Pentominoes?</h3>

<p>Pentominoes are polyominoes made of 5 unit squares connected edge to edge. Ignoring rotations and reflections, there are <strong>12</strong> distinct pentominoes, typically named after letters they resemble: F, I, L, P, N, T, U, V, W, X, Y, Z:</p>

<p><img src="/images/pentominoes.svg" alt="12 pentomino shapes" style="width:100%; max-width:840px;" /></p>

<h3 id="problem-definition">Problem Definition</h3>

<p><strong>Classic problem</strong>: Can you tile a 6×10 rectangle using all 12 pentominoes, each exactly once?</p>

<p>This generalizes to other rectangle sizes:</p>
<ul>
  <li>5×12</li>
  <li>4×15</li>
  <li>3×20</li>
</ul>

<!--more-->

<h2 id="from-tiling-to-exact-cover">From Tiling to Exact Cover</h2>

<p>Dancing Links solves <strong>exact cover problems</strong>:</p>

<blockquote>
  <p>Given a 0-1 matrix, select a set of rows such that every column contains exactly one 1.</p>
</blockquote>

<h3 id="constructing-the-matrix">Constructing the Matrix</h3>

<p>For pentomino tiling, we build the following matrix:</p>

<p><strong>Columns</strong>:</p>
<ul>
  <li>Each grid cell (6×10 = 60 columns)</li>
  <li>Each pentomino type (12 columns)</li>
  <li>Total: 72 columns</li>
</ul>

<p><strong>Rows</strong>:</p>
<ul>
  <li>For each pentomino type</li>
  <li>Consider all valid placements (position + orientation) within the rectangle</li>
  <li>Each row = one “placement option”</li>
  <li>The 1s mark occupied cells and the piece type used</li>
</ul>

<h3 id="example">Example</h3>

<p>Suppose we place the <strong>L pentomino</strong> at position (0,0):</p>

<p><img src="/images/pentomino-L-example.svg" alt="L pentomino placement example" style="width:100%; max-width:300px;" /></p>

<p>Row: <code class="language-plaintext highlighter-rouge">[L-type, (0,0), (1,0), (2,0), (3,0), (3,1)]</code> — six 1s.</p>

<h2 id="the-dancing-links-algorithm">The Dancing Links Algorithm</h2>

<p>DLX uses a <strong>doubly-linked circular list</strong> (a “toroidal” structure) to represent the sparse matrix, enabling efficient backtracking:</p>

<h3 id="data-structure">Data Structure</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ColumnHeader ↔ ColumnHeader ↔ ColumnHeader ↔ ...
       ↓              ↓               ↓
    DataNode ↔ DataNode ↔ DataNode
       ↓
    DataNode
</code></pre></div></div>

<h3 id="algorithm-flow">Algorithm Flow</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Algorithm X(cover):
    if matrix empty:
        solution found!
    choose column c (fewest options)
    cover column c
    for each row r in column c:
        add r to solution
        for each column j in row r:
            cover column j
        X(cover)
        remove r from solution
        for each column j in row r:
            uncover column j
    uncover column c
</code></pre></div></div>

<h3 id="optimization-minimum-remaining-values">Optimization: Minimum Remaining Values</h3>

<p>The key optimization of DLX — <strong>always choose the column with the fewest options</strong> — maximizes pruning.</p>

<h2 id="core-implementation">Core Implementation</h2>

<p>The heart of DLX lies in the <code class="language-plaintext highlighter-rouge">cover</code> and <code class="language-plaintext highlighter-rouge">uncover</code> operations, which use doubly-linked lists for efficient backtracking:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">cover</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">col</span><span class="p">):</span>
    <span class="s">"""Cover column col: remove column and all intersecting rows from matrix"""</span>
    <span class="c1"># 1. Remove column from column list
</span>    <span class="bp">self</span><span class="p">.</span><span class="n">R</span><span class="p">[</span><span class="bp">self</span><span class="p">.</span><span class="n">L</span><span class="p">[</span><span class="n">col</span><span class="p">]]</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">R</span><span class="p">[</span><span class="n">col</span><span class="p">]</span>
    <span class="bp">self</span><span class="p">.</span><span class="n">L</span><span class="p">[</span><span class="bp">self</span><span class="p">.</span><span class="n">R</span><span class="p">[</span><span class="n">col</span><span class="p">]]</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">L</span><span class="p">[</span><span class="n">col</span><span class="p">]</span>

    <span class="c1"># 2. Remove all rows that intersect this column
</span>    <span class="n">i</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">D</span><span class="p">[</span><span class="n">col</span><span class="p">]</span>
    <span class="k">while</span> <span class="n">i</span> <span class="o">!=</span> <span class="n">col</span><span class="p">:</span>
        <span class="n">j</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">R</span><span class="p">[</span><span class="n">i</span><span class="p">]</span>
        <span class="k">while</span> <span class="n">j</span> <span class="o">!=</span> <span class="n">i</span><span class="p">:</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">D</span><span class="p">[</span><span class="bp">self</span><span class="p">.</span><span class="n">U</span><span class="p">[</span><span class="n">j</span><span class="p">]]</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">D</span><span class="p">[</span><span class="n">j</span><span class="p">]</span>  <span class="c1"># Skip node j
</span>            <span class="bp">self</span><span class="p">.</span><span class="n">U</span><span class="p">[</span><span class="bp">self</span><span class="p">.</span><span class="n">D</span><span class="p">[</span><span class="n">j</span><span class="p">]]</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">U</span><span class="p">[</span><span class="n">j</span><span class="p">]</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">S</span><span class="p">[</span><span class="bp">self</span><span class="p">.</span><span class="n">C</span><span class="p">[</span><span class="n">j</span><span class="p">]]</span> <span class="o">-=</span> <span class="mi">1</span>  <span class="c1"># Decrement column count
</span>            <span class="n">j</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">R</span><span class="p">[</span><span class="n">j</span><span class="p">]</span>
        <span class="n">i</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">D</span><span class="p">[</span><span class="n">i</span><span class="p">]</span>

<span class="k">def</span> <span class="nf">uncover</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">col</span><span class="p">):</span>
    <span class="s">"""Uncover column col: exact reverse of cover operation"""</span>
    <span class="c1"># 1. Restore all removed rows (in reverse order)
</span>    <span class="n">i</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">U</span><span class="p">[</span><span class="n">col</span><span class="p">]</span>
    <span class="k">while</span> <span class="n">i</span> <span class="o">!=</span> <span class="n">col</span><span class="p">:</span>
        <span class="n">j</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">L</span><span class="p">[</span><span class="n">i</span><span class="p">]</span>
        <span class="k">while</span> <span class="n">j</span> <span class="o">!=</span> <span class="n">i</span><span class="p">:</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">S</span><span class="p">[</span><span class="bp">self</span><span class="p">.</span><span class="n">C</span><span class="p">[</span><span class="n">j</span><span class="p">]]</span> <span class="o">+=</span> <span class="mi">1</span>  <span class="c1"># Restore column count
</span>            <span class="bp">self</span><span class="p">.</span><span class="n">D</span><span class="p">[</span><span class="bp">self</span><span class="p">.</span><span class="n">U</span><span class="p">[</span><span class="n">j</span><span class="p">]]</span> <span class="o">=</span> <span class="n">j</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">U</span><span class="p">[</span><span class="bp">self</span><span class="p">.</span><span class="n">D</span><span class="p">[</span><span class="n">j</span><span class="p">]]</span> <span class="o">=</span> <span class="n">j</span>
            <span class="n">j</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">L</span><span class="p">[</span><span class="n">j</span><span class="p">]</span>
        <span class="n">i</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">U</span><span class="p">[</span><span class="n">i</span><span class="p">]</span>

    <span class="c1"># 2. Restore column to column list
</span>    <span class="bp">self</span><span class="p">.</span><span class="n">R</span><span class="p">[</span><span class="bp">self</span><span class="p">.</span><span class="n">L</span><span class="p">[</span><span class="n">col</span><span class="p">]]</span> <span class="o">=</span> <span class="n">col</span>
    <span class="bp">self</span><span class="p">.</span><span class="n">L</span><span class="p">[</span><span class="bp">self</span><span class="p">.</span><span class="n">R</span><span class="p">[</span><span class="n">col</span><span class="p">]]</span> <span class="o">=</span> <span class="n">col</span>
</code></pre></div></div>

<p><strong>Key Points</strong>:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">cover</code> operation must be <strong>reversible</strong> for exact backtracking</li>
  <li>Use <code class="language-plaintext highlighter-rouge">L</code> and <code class="language-plaintext highlighter-rouge">R</code> pointers to skip nodes horizontally</li>
  <li>Use <code class="language-plaintext highlighter-rouge">U</code> and <code class="language-plaintext highlighter-rouge">D</code> pointers to skip nodes vertically</li>
  <li><code class="language-plaintext highlighter-rouge">S</code> array tracks node count per column for MRV heuristic</li>
</ul>

<p>Full implementation available at: <a href="https://github.com/morefreeze/morefreeze.github.io/blob/master/code/pentomino_dlx.py">GitHub - pentomino_dlx.py</a></p>

<h2 id="sample-output">Sample Output</h2>

<p><img src="/images/pentomino-6x10-solution.svg" alt="6x10 pentomino tiling solution" style="width:100%; max-width:480px;" /></p>

<p><em>A solution tiling a 6×10 rectangle with all 12 pentominoes</em></p>

<h2 id="complexity-analysis">Complexity Analysis</h2>

<h3 id="space-complexity">Space Complexity</h3>

<ul>
  <li>For 6×10 rectangle, each column has \(O(n)\) nodes at most</li>
  <li>Total nodes ≈ number of placement options</li>
  <li>Space: \(O(\text{placements})\)</li>
</ul>

<h3 id="time-complexity">Time Complexity</h3>

<ul>
  <li>Worst case: exponential \(O(2^n)\)</li>
  <li>In practice, DLX’s pruning is very effective</li>
  <li>Pentomino problems typically solve in milliseconds</li>
</ul>

<h2 id="extensions-and-variations">Extensions and Variations</h2>

<h3 id="1-other-polyominoes">1. Other Polyominoes</h3>

<ul>
  <li><strong>Trominoes</strong>: 2 types (straight, L-shaped)</li>
  <li><strong>Tetrominoes</strong>: 5 types</li>
  <li><strong>Hexominoes</strong>: 35 types — interestingly, all 35 free hexominoes <strong>cannot</strong> tile any rectangle. A checkerboard parity argument proves this: 24 “odd” hexominoes cover 3 black + 3 white, while 11 “even” ones cover 4 black + 2 white (or vice versa), so total black squares covered is always even; yet any 10×21 rectangle has 105 black squares (odd) — a contradiction. However, they can tile regions with holes (e.g., a 15×15 square with a central 3×5 removed). Full code at <a href="https://github.com/morefreeze/morefreeze.github.io/blob/master/code/hexomino_dlx.py">hexomino_dlx.py</a>.</li>
</ul>

<h3 id="2-counting-problems">2. Counting Problems</h3>

<p>According to the literature, the 6×10 pentomino tiling has <strong>9,356</strong> distinct solutions (counting rotations and reflections). This is a classic result in combinatorial mathematics that has been verified by multiple different methods.</p>

<h3 id="3-variants">3. Variants</h3>

<ul>
  <li>Rectangles with holes</li>
  <li>Non-rectangular regions</li>
  <li>Using a subset of pieces</li>
  <li>Allowing repeated pieces</li>
</ul>

<h2 id="conclusion">Conclusion</h2>

<p>The pentomino tiling problem perfectly showcases the power of Dancing Links:</p>

<ol>
  <li><strong>Elegant Modeling</strong>: Transform geometric problems into exact cover</li>
  <li><strong>Efficient Algorithm</strong>: Doubly-linked lists enable fast backtracking</li>
  <li><strong>Highly Extensible</strong>: Generalizes to other constraint satisfaction problems</li>
</ol>

<p>This embodies Knuth’s design philosophy: <strong>Find the essential representation of the problem, then implement it with the most appropriate data structure.</strong></p>

<h2 id="references">References</h2>

<ul>
  <li>Knuth, D. E. (2022). <em>The Art of Computer Programming, Volume 4B: Combinatorial Algorithms, Part 2</em>. Addison-Wesley.</li>
  <li><a href="https://en.wikipedia.org/wiki/Dancing_Links">Dancing Links - Wikipedia</a></li>
  <li><a href="https://en.wikipedia.org/wiki/Exact_cover">Exact Cover - Wikipedia</a></li>
</ul>

<hr />]]></content><author><name>More Freeze</name></author><category term="algorithm" /><category term="combinatorics" /><category term="DLX" /><category term="algorithm" /><category term="combinatorics" /><category term="TAOCP" /><category term="exact-cover" /><summary type="html"><![CDATA[Exploring the pentomino tiling problem from TAOCP, learning how to transform a tiling problem into exact cover using the Dancing Links algorithm]]></summary></entry><entry><title type="html">博客搬家：从 GitHub Pages 到自定义域名</title><link href="https://blog.morefreeze.top/2026/03/new-domain.html" rel="alternate" type="text/html" title="博客搬家：从 GitHub Pages 到自定义域名" /><published>2026-03-25T00:00:00+00:00</published><updated>2026-03-25T00:00:00+00:00</updated><id>https://blog.morefreeze.top/2026/03/new-domain</id><content type="html" xml:base="https://blog.morefreeze.top/2026/03/new-domain.html"><![CDATA[<p>最近做了一件拖了很久的事情——给博客换了个域名。新地址是 <strong>blog.morefreeze.top</strong>，旧的 <code class="language-plaintext highlighter-rouge">morefreeze.github.io</code> 会自动跳转过来，不影响之前收藏的链接。</p>

<!--more-->

<h2 id="为什么要换">为什么要换</h2>

<p>用了好几年 <code class="language-plaintext highlighter-rouge">morefreeze.github.io</code>，其实一直没什么不满。GitHub Pages 免费、稳定、部署方便，对个人博客来说绰绰有余。</p>

<p>但说实话，换域名这件事没什么深思熟虑——<code class="language-plaintext highlighter-rouge">.top</code> 域名一年几块钱，某天刷到续费提醒，顺手就买了。买都买了，不用也浪费。</p>

<p>真要说理由的话，大概就是<strong>拥有感</strong>。<code class="language-plaintext highlighter-rouge">github.io</code> 后缀终究是别人家的地盘，哪天 GitHub 改策略了，连个跳转都不一定给你留。有个自己的域名，至少迁移的时候 DNS 一改就行，不用求人。</p>

<h2 id="迁移过程">迁移过程</h2>

<p>整个过程比想象中简单，主要就三步：</p>

<h3 id="1-买域名">1. 买域名</h3>

<p>在域名注册商买了 <code class="language-plaintext highlighter-rouge">morefreeze.top</code>，然后配置 DNS 解析，添加一条 CNAME 记录：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>blog.morefreeze.top  →  morefreeze.github.io
</code></pre></div></div>

<h3 id="2-配置-github-pages">2. 配置 GitHub Pages</h3>

<p>在仓库根目录创建 <code class="language-plaintext highlighter-rouge">CNAME</code> 文件，内容就一行：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>blog.morefreeze.top
</code></pre></div></div>

<p>然后在 GitHub 仓库的 Settings → Pages 里确认 Custom domain 已经生效，顺便勾上 Enforce HTTPS。</p>

<h3 id="3-更新站点配置">3. 更新站点配置</h3>

<p>把 <code class="language-plaintext highlighter-rouge">_config.yml</code> 里的 <code class="language-plaintext highlighter-rouge">url</code> 改成新域名：</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">url</span><span class="pi">:</span> <span class="s2">"</span><span class="s">https://blog.morefreeze.top"</span>
</code></pre></div></div>

<p>推送后等 DNS 生效（通常几分钟到几小时），就搞定了。</p>

<h2 id="注意事项">注意事项</h2>

<p>迁移过程中有几个值得注意的点：</p>

<ul>
  <li><strong>旧链接不会失效</strong>。GitHub Pages 默认会把 <code class="language-plaintext highlighter-rouge">morefreeze.github.io</code> 的请求 301 到自定义域名，所以之前被搜索引擎收录的页面、别人分享的链接都不会变成死链。</li>
  <li><strong>HTTPS 证书</strong>。GitHub Pages 会自动为自定义域名申请 Let’s Encrypt 证书，不需要自己折腾。</li>
  <li><strong>RSS 订阅</strong>。如果有人通过 RSS 订阅了博客，feed 地址会变，但大部分 RSS 阅读器能自动跟随跳转。</li>
</ul>

<h2 id="最后">最后</h2>

<p>域名这件事，早做早省心。拖了这么久，实际操作不到半小时就搞定了。如果你也在用 GitHub Pages 写博客，强烈建议花几块钱买个自己的域名——毕竟在互联网上，域名就是你的门牌号。</p>

<p>新地址：<a href="https://blog.morefreeze.top">blog.morefreeze.top</a></p>

<p>欢迎更新书签 :)</p>]]></content><author><name>More Freeze</name></author><category term="life" /><category term="博客" /><category term="域名" /><category term="GitHub Pages" /><category term="DNS" /><summary type="html"><![CDATA[记录博客迁移到 blog.morefreeze.top 的过程和思考]]></summary></entry><entry><title type="html">谁养斑马？——用算法终结逻辑推理题</title><link href="https://blog.morefreeze.top/2026/03/zebra-puzzle.html" rel="alternate" type="text/html" title="谁养斑马？——用算法终结逻辑推理题" /><published>2026-03-25T00:00:00+00:00</published><updated>2026-03-25T00:00:00+00:00</updated><id>https://blog.morefreeze.top/2026/03/zebra-puzzle</id><content type="html" xml:base="https://blog.morefreeze.top/2026/03/zebra-puzzle.html"><![CDATA[<p>我花了一个小时没解出来，然后写了 30 行代码，算法用不到 1 毫秒解完了。</p>

<p>这道题就是著名的”斑马谜题”（又叫爱因斯坦谜题）：五个人住一排房子，每人国籍不同、职业不同、宠物不同、饮料不同、房子颜色不同，给你一堆线索，问——<strong>谁养斑马？</strong></p>

<p>1962 年发表在 <em>Life International</em> 杂志上，据说只有 2% 的人能解出来。</p>

<p>这篇文章介绍一种完全不同的做法：<strong>把谜题翻译成 XCC 问题，让舞蹈链算法自动求解</strong>。不用画表格，不用推理，只需要把”什么是合法答案”描述清楚。</p>

<p><strong>然后我们反过来——让算法自动出一道新的逻辑推理题。</strong></p>

<!--more-->

<hr />

<h2 id="谜面">谜面</h2>

<p>完整的 16 条线索（含两条隐含线索）：</p>

<ol>
  <li>英国人住红色房子</li>
  <li>黄色房子里住外交官</li>
  <li>画家来自日本</li>
  <li>喝咖啡的人住绿色房子</li>
  <li>挪威人住最左边</li>
  <li>西班牙人养狗</li>
  <li>中间房子的人喝牛奶</li>
  <li>小提琴手喝橙汁</li>
  <li>白色房子紧挨在绿色房子左边</li>
  <li>乌克兰人喝茶</li>
  <li>马住在外交官隔壁</li>
  <li>雕塑家养蜗牛</li>
  <li>挪威人住蓝色房子隔壁</li>
  <li>护士住在狐狸隔壁</li>
  <li>有人养斑马（隐含）</li>
  <li>有人喝水（隐含）</li>
</ol>

<p>问：谁养斑马？谁喝水？</p>

<hr />

<h2 id="手工解法的痛苦">手工解法的痛苦</h2>

<p>5 个类别，每个 5 个值，分配到 5 个房子。如果完全没有约束，可能的分配方案有 \((5!)^5 = 24{,}883{,}200{,}000\) 种。</p>

<p>手工做法是画一个排除矩阵，逐步收窄范围。但”隔壁”类线索（第 9、11、13、14 条）特别难处理——处理它们时要同时维护多条分支推导链，基本超出人类工作记忆上限。一旦某条分支出错，整张表作废重来。</p>

<p>我们需要一种不怕分支、不怕回溯的方法。</p>

<hr />

<h2 id="把线索翻译成-xcc">把线索翻译成 XCC</h2>

<p>前两篇（<a href="/2026/03/dlx-colors.html">颜色项与贴纸</a>、<a href="/2026/03/dlx-xcc.html">拼字谜题</a>）介绍了 XCC 的核心思想及其应用：<strong>主要项必须恰好覆盖一次，颜色项可以被多次覆盖但颜色必须一致</strong>。颜色项用来表示”同一个位置的属性槽”——多条线索可以同时约束它，只要它们给出的颜色（属性值）相同，算法就不会报冲突。</p>

<p>斑马谜题的翻译规则非常直接：</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">概念</th>
      <th style="text-align: left">XCC 中的角色</th>
      <th style="text-align: left">说明</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left">每条线索</td>
      <td style="text-align: left"><strong>主要项</strong></td>
      <td style="text-align: left">每条线索必须恰好被满足一次</td>
    </tr>
    <tr>
      <td style="text-align: left">每个房子的每个属性槽</td>
      <td style="text-align: left"><strong>颜色项</strong></td>
      <td style="text-align: left">\(N_j, J_j, P_j, D_j, C_j\)（N=国籍, J=职业, P=宠物, D=饮料, C=颜色；\(j=0..4\)）</td>
    </tr>
  </tbody>
</table>

<p>颜色项的 color 值就是具体的属性值。比如”\(N_2\) 的颜色是 England”表示”2 号房子住着英国人”。</p>

<p>通用 CSP 库当然也能解，但 XCC 框架的好处是：颜色项天然支持”多条线索约束同一个槽位”的语义，而且解题和出题使用相同的建模模式，调用同一个求解器——后面会看到这一点。</p>

<h3 id="线索怎么变成-option">线索怎么变成 option？</h3>

<p>以线索 1（”英国人住红色房子”）为例。我们不知道英国人住哪号房子，所以要为每种可能性生成一个 option：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#1  N0:England  C0:red     ← 英国人住 0 号房子（红色）
#1  N1:England  C1:red     ← 英国人住 1 号房子（红色）
#1  N2:England  C2:red     ← 英国人住 2 号房子（红色）
#1  N3:England  C3:red     ← 英国人住 3 号房子（红色）
#1  N4:England  C4:red     ← 英国人住 4 号房子（红色）
</code></pre></div></div>

<p>每个 option 包含一个主要项（<code class="language-plaintext highlighter-rouge">#1</code>，表示”线索 1 被满足”）和若干颜色项（表示”这些属性槽被设置为这些值”）。</p>

<p>算法会从这 5 个 option 里选恰好一个——选中后，对应的颜色项就被”锁定”了。如果后续某条线索试图把同一个 \(N_j\) 涂成不同颜色，算法自动回溯。</p>

<h3 id="隔壁类线索">“隔壁”类线索</h3>

<p>线索 11（”马住在外交官隔壁”）稍微复杂。”隔壁”意味着两种可能：马在左外交官在右，或者反过来。对每对相邻位置 \((i, i+1)\) 都要列举两种情况：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#11  P0:horse  J1:diplomat    ← 马在 0 号，外交官在 1 号
#11  J0:diplomat  P1:horse    ← 外交官在 0 号，马在 1 号
#11  P1:horse  J2:diplomat    ← 马在 1 号，外交官在 2 号
...
</code></pre></div></div>

<p>4 对相邻位置 × 2 种方向 = 8 个 option。</p>

<h3 id="固定位置线索">固定位置线索</h3>

<p>线索 5（”挪威人住最左边”）最简单——只有一个 option：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#5  N0:Norway
</code></pre></div></div>

<h3 id="总计">总计</h3>

<p>16 条线索共生成 <strong>80 个 option</strong>。构建完毕，调用上一篇的 <code class="language-plaintext highlighter-rouge">DLX_C</code> 求解器，瞬间得到唯一解。</p>

<hr />

<h2 id="代码">代码</h2>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">dlx_colors</span> <span class="kn">import</span> <span class="n">DLX_C</span>

<span class="c1"># 16 个 primary items（线索），25 个 secondary items（属性槽）
</span><span class="n">num_primary</span> <span class="o">=</span> <span class="mi">16</span>
<span class="n">sec_base</span> <span class="o">=</span> <span class="mi">16</span>
<span class="k">def</span> <span class="nf">sec_idx</span><span class="p">(</span><span class="n">cat</span><span class="p">,</span> <span class="n">house</span><span class="p">):</span>  <span class="c1"># cat: 0=N,1=J,2=P,3=D,4=C
</span>    <span class="k">return</span> <span class="n">sec_base</span> <span class="o">+</span> <span class="n">cat</span> <span class="o">*</span> <span class="mi">5</span> <span class="o">+</span> <span class="n">house</span>

<span class="c1"># 线索 1: 英国人住红色房子（5 个 option，clue_id=0）
</span><span class="k">for</span> <span class="n">j</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">5</span><span class="p">):</span>
    <span class="n">add_option</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="sa">f</span><span class="s">"#1 N</span><span class="si">{</span><span class="n">j</span><span class="si">}</span><span class="s">:England C</span><span class="si">{</span><span class="n">j</span><span class="si">}</span><span class="s">:red"</span><span class="p">,</span>
               <span class="p">[(</span><span class="n">sec_idx</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">j</span><span class="p">),</span> <span class="s">'England'</span><span class="p">),</span> <span class="p">(</span><span class="n">sec_idx</span><span class="p">(</span><span class="mi">4</span><span class="p">,</span> <span class="n">j</span><span class="p">),</span> <span class="s">'red'</span><span class="p">)])</span>

<span class="c1"># 线索 9: 白色房子紧挨在绿色房子左边（4 个 option，clue_id=8）
</span><span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">4</span><span class="p">):</span>
    <span class="n">add_option</span><span class="p">(</span><span class="mi">8</span><span class="p">,</span> <span class="sa">f</span><span class="s">"#9 C</span><span class="si">{</span><span class="n">i</span><span class="si">}</span><span class="s">:white C</span><span class="si">{</span><span class="n">i</span><span class="o">+</span><span class="mi">1</span><span class="si">}</span><span class="s">:green"</span><span class="p">,</span>
               <span class="p">[(</span><span class="n">sec_idx</span><span class="p">(</span><span class="mi">4</span><span class="p">,</span> <span class="n">i</span><span class="p">),</span> <span class="s">'white'</span><span class="p">),</span> <span class="p">(</span><span class="n">sec_idx</span><span class="p">(</span><span class="mi">4</span><span class="p">,</span> <span class="n">i</span><span class="o">+</span><span class="mi">1</span><span class="p">),</span> <span class="s">'green'</span><span class="p">)])</span>

<span class="c1"># 线索 11: 马住在外交官隔壁（8 个 option，clue_id=10）
</span><span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">4</span><span class="p">):</span>
    <span class="n">add_option</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span> <span class="sa">f</span><span class="s">"#11 P</span><span class="si">{</span><span class="n">i</span><span class="si">}</span><span class="s">:horse J</span><span class="si">{</span><span class="n">i</span><span class="o">+</span><span class="mi">1</span><span class="si">}</span><span class="s">:diplomat"</span><span class="p">,</span>
               <span class="p">[(</span><span class="n">sec_idx</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="n">i</span><span class="p">),</span> <span class="s">'horse'</span><span class="p">),</span> <span class="p">(</span><span class="n">sec_idx</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="n">i</span><span class="o">+</span><span class="mi">1</span><span class="p">),</span> <span class="s">'diplomat'</span><span class="p">)])</span>
    <span class="n">add_option</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span> <span class="sa">f</span><span class="s">"#11 J</span><span class="si">{</span><span class="n">i</span><span class="si">}</span><span class="s">:diplomat P</span><span class="si">{</span><span class="n">i</span><span class="o">+</span><span class="mi">1</span><span class="si">}</span><span class="s">:horse"</span><span class="p">,</span>
               <span class="p">[(</span><span class="n">sec_idx</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="n">i</span><span class="p">),</span> <span class="s">'diplomat'</span><span class="p">),</span> <span class="p">(</span><span class="n">sec_idx</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="n">i</span><span class="o">+</span><span class="mi">1</span><span class="p">),</span> <span class="s">'horse'</span><span class="p">)])</span>
</code></pre></div></div>

<p>完整实现见 <a href="https://github.com/morefreeze/morefreeze.github.io/blob/master/code/zebra_puzzle.py">zebra_puzzle.py</a>。运行结果：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>       House       0       1       2       3       4
  --------------------------------------------------------
 Nationality  Norway Ukraine England   Spain   Japan
         Job diplomat   nurse sculptor violinist painter
         Pet     fox   horse  snails     dog   zebra
       Drink   water     tea    milk      oj  coffee
       Color  yellow    blue     red   white   green
</code></pre></div></div>

<p><strong>日本人养斑马，挪威人喝水。</strong></p>

<p><img src="/images/zebra-puzzle-solution.svg" alt="Zebra Puzzle Solution" /></p>

<hr />

<h2 id="反过来自动出题">反过来：自动出题</h2>

<p>解题是”给定线索，求分配”。反过来——<strong>给定一个分配，找最少的线索使得解唯一</strong>——就是自动出题。</p>

<h3 id="算法">算法</h3>

<ol>
  <li><strong>随机生成一个合法分配</strong>：每个类别的 5 个值随机排列到 5 个房子</li>
  <li><strong>枚举所有可能的线索</strong>：
    <ul>
      <li>同房线索：\(\binom{5}{2} \times 5 = 50\) 条（”某属性 A 和属性 B 在同一房子”）</li>
      <li>相邻线索：\(\binom{5}{2} \times 4 \times 2 + 5 \times 4 = 100\) 条（”属性 A 和属性 B 在相邻房子”）</li>
      <li>固定位置线索：\(5 \times 5 = 25\) 条（”某属性在第 k 号房子”）</li>
    </ul>
  </li>
  <li><strong>贪心移除</strong>：打乱线索顺序，逐条尝试删除。如果删除后仍有唯一解（用 XCC 验证），就删掉；否则保留</li>
</ol>

<p>这不保证得到全局最少的线索集（那是 NP-hard 的），但实际效果很好——通常能压缩到 15～20 条线索。</p>

<h3 id="一个例子">一个例子</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">clues</span><span class="p">,</span> <span class="n">answer</span> <span class="o">=</span> <span class="n">generate_puzzle</span><span class="p">(</span><span class="n">seed</span><span class="o">=</span><span class="mi">42</span><span class="p">)</span>
</code></pre></div></div>

<p>输出：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> 1. The person with Nationality=Canadian also has Color=green.
 2. The person with Drink=coffee also has Color=blue.
 3. The person with Nationality=French lives next to the person with Drink=soda.
 4. The person with Job=lawyer also has Color=yellow.
 5. The person with Job=teacher lives next to the person with Job=artist.
 6. The person with Job=artist also has Pet=cat.
 7. The person with Job=lawyer also has Drink=juice.
 8. The person with Nationality=British lives next to the person with Job=chef.
 9. The person with Pet=hamster also has Drink=coffee.
10. The person with Drink=water lives in the fourth house.
11. The person with Job=lawyer lives next to the person with Color=green.
12. The person with Pet=bird also has Color=white.
13. The person with Nationality=British also has Job=lawyer.
14. The person with Nationality=Dutch lives in the first house.
15. The person with Pet=bird lives next to the person with Pet=dog.
16. The person with Nationality=British lives next to the person with Job=teacher.
17. The person with Drink=juice lives in the second house.
</code></pre></div></div>

<p>17 条线索，唯一解。你可以拿去考朋友。</p>

<h3 id="关键代码">关键代码</h3>

<p>出题和解题使用相同的 XCC 建模模式。区别只在于：出题时需要额外的 <strong>all-different</strong> 主要项——因为现在线索不够多，算法需要显式知道”每个属性值恰好出现一次”：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># 每个属性值对应一个 primary item，5 个 option（放在哪个房子）
</span><span class="k">for</span> <span class="n">cat_i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">num_cats</span><span class="p">):</span>
    <span class="k">for</span> <span class="n">v_i</span><span class="p">,</span> <span class="n">val</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">values</span><span class="p">[</span><span class="n">cat_i</span><span class="p">]):</span>
        <span class="k">for</span> <span class="n">j</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">n</span><span class="p">):</span>
            <span class="n">options</span><span class="p">.</span><span class="n">append</span><span class="p">([</span>
                <span class="p">(</span><span class="n">ad_idx</span><span class="p">(</span><span class="n">cat_i</span><span class="p">,</span> <span class="n">v_i</span><span class="p">),</span> <span class="bp">None</span><span class="p">),</span>   <span class="c1"># primary: 该值必须被分配
</span>                <span class="p">(</span><span class="n">sec_idx</span><span class="p">(</span><span class="n">cat_i</span><span class="p">,</span> <span class="n">j</span><span class="p">),</span> <span class="n">val</span><span class="p">)</span>       <span class="c1"># secondary: 放在第 j 号房子
</span>            <span class="p">])</span>
</code></pre></div></div>

<p>这正是 Knuth 在 Exercise 100(c) 里讲的 <strong>CSP → XCC 通用翻译</strong>，斑马谜题（Exercise 101）就是它的经典应用：每个变量的每种取值是一个 option，约束通过颜色项的一致性自动保证。</p>

<hr />

<h2 id="knuth-的小优化">Knuth 的小优化</h2>

<p>Knuth 在 TAOCP 4B 的答案里提到一个技巧：上面的建模<strong>没有</strong>显式约束”每个属性值只能出现在一个房子”。比如，算法并不直接知道”如果 England 在 2 号房子，就不能再出现在别的房子”——它只是碰巧从 16 条线索的交叉约束中推导出这一点。</p>

<p>如果额外加 25 个<strong>逆映射</strong>颜色项（每个属性值一个，颜色 = 房子编号），让算法更早感知到冲突，搜索树就从 <strong>112 个节点</strong>缩减到 <strong>32 个</strong>。代码里加几行就行，但节省的时间”刚好抵消”了这些额外项带来的开销——Knuth 原话。</p>

<hr />

<h2 id="回顾">回顾</h2>

<p>这是本系列第三篇。前两篇分别是<a href="/2026/03/dlx-colors.html">颜色项与贴纸</a>和<a href="/2026/03/dlx-xcc.html">拼字谜题</a>，建议按顺序阅读。</p>

<p>四篇文章用同一个求解器（<code class="language-plaintext highlighter-rouge">DLX_C</code>）解了四类完全不同的问题：</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">文章</th>
      <th style="text-align: left">问题</th>
      <th style="text-align: left">主要项</th>
      <th style="text-align: left">颜色项</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><a href="/2026/03/dlx-colors.html">颜色项与贴纸</a></td>
      <td style="text-align: left">颜色项的 bug 与修复</td>
      <td style="text-align: left">每个单词</td>
      <td style="text-align: left">每个格子（颜色=字母）</td>
    </tr>
    <tr>
      <td style="text-align: left"><a href="/2026/03/dlx-xcc.html">拼字谜题</a></td>
      <td style="text-align: left">把单词塞进格子</td>
      <td style="text-align: left">每个单词</td>
      <td style="text-align: left">每个格子（颜色=字母）</td>
    </tr>
    <tr>
      <td style="text-align: left">本篇（解题）</td>
      <td style="text-align: left">满足逻辑线索</td>
      <td style="text-align: left">每条线索</td>
      <td style="text-align: left">每个属性槽（颜色=属性值）</td>
    </tr>
    <tr>
      <td style="text-align: left">本篇（出题）</td>
      <td style="text-align: left">找最少线索</td>
      <td style="text-align: left">线索 + all-different</td>
      <td style="text-align: left">同上</td>
    </tr>
  </tbody>
</table>

<p>四类问题表面上毫无关联，底层却是同一种结构：描述”什么是合法选择”，让算法找到覆盖全局的那一组。</p>

<p>我们没有写过任何”如何推理”的代码。每次只是换一种方式描述”什么是合法答案”，算法就自动找到了所有答案。</p>

<p>这就是 XCC 最迷人的地方：<strong>你负责描述问题，它负责求解</strong>。</p>

<hr />

<h2 id="完整代码">完整代码</h2>

<p><a href="https://github.com/morefreeze/morefreeze.github.io/blob/master/code/zebra_puzzle.py">zebra_puzzle.py</a> 包含解题和出题两部分，依赖 <a href="https://github.com/morefreeze/morefreeze.github.io/blob/master/code/dlx_colors.py">dlx_colors.py</a>，直接运行即可。</p>

<p>换个随机种子就能生成一道新题，拿去考朋友试试。</p>]]></content><author><name>More Freeze</name></author><category term="algorithm" /><category term="knuth" /><category term="exact-cover" /><category term="dancing-links" /><category term="CSP" /><category term="zebra-puzzle" /><category term="TAOCP" /><summary type="html"><![CDATA[经典的爱因斯坦谜题，手工画表格能把人逼疯。把它翻译成 XCC 问题之后，算法几毫秒就给出唯一解——然后我们反过来，让算法自动出题]]></summary></entry><entry><title type="html">Who Owns the Zebra? Solving Logic Puzzles with Algorithms</title><link href="https://blog.morefreeze.top/2026/03/zebra-puzzle_en.html" rel="alternate" type="text/html" title="Who Owns the Zebra? Solving Logic Puzzles with Algorithms" /><published>2026-03-25T00:00:00+00:00</published><updated>2026-03-25T00:00:00+00:00</updated><id>https://blog.morefreeze.top/2026/03/zebra-puzzle_en</id><content type="html" xml:base="https://blog.morefreeze.top/2026/03/zebra-puzzle_en.html"><![CDATA[<p>I spent an hour without solving it, then wrote 30 lines of code, and the algorithm solved it in under a millisecond.</p>

<p>This is the famous “Zebra Puzzle” (also known as the Einstein Riddle): five people live in a row of houses, each with a different nationality, occupation, pet, drink, and house color. You’re given a bunch of clues and asked—<strong>who owns the zebra?</strong></p>

<p>First published in <em>Life International</em> magazine in 1962, legend has it that only 2% of people can solve it.</p>

<p>This article introduces a completely different approach: <strong>translate the puzzle into an XCC problem and let the Dancing Links algorithm solve it automatically</strong>. No drawing tables, no manual reasoning—just describe “what constitutes a valid answer.”</p>

<p><strong>Then we reverse it—letting the algorithm automatically generate a new logic puzzle.</strong></p>

<!--more-->

<hr />

<h2 id="the-puzzle">The Puzzle</h2>

<p>Here are the complete 16 clues (including two implicit ones):</p>

<ol>
  <li>The Englishman lives in the red house</li>
  <li>The diplomat lives in the yellow house</li>
  <li>The painter is from Japan</li>
  <li>The coffee drinker lives in the green house</li>
  <li>The Norwegian lives in the leftmost house</li>
  <li>The Spaniard owns the dog</li>
  <li>The person in the middle house drinks milk</li>
  <li>The violinist drinks orange juice</li>
  <li>The white house is immediately to the left of the green house</li>
  <li>The Ukrainian drinks tea</li>
  <li>The horse lives next to the diplomat</li>
  <li>The sculptor owns snails</li>
  <li>The Norwegian lives next to the blue house</li>
  <li>The nurse lives next to the fox</li>
  <li>Someone owns a zebra (implicit)</li>
  <li>Someone drinks water (implicit)</li>
</ol>

<p>Question: Who owns the zebra? Who drinks water?</p>

<hr />

<h2 id="the-pain-of-manual-solving">The Pain of Manual Solving</h2>

<p>Five categories, each with five values, assigned to five houses. Without constraints, there are \((5!)^5 = 24{,}883{,}200{,}000\) possible assignments.</p>

<p>The manual approach involves drawing an elimination matrix and gradually narrowing the scope. But “next door” type clues (9, 11, 13, 14) are particularly difficult to handle—processing them requires maintaining multiple branches of deduction chains, basically exceeding human working memory limits. Once one branch goes wrong, the entire table is void and you start over.</p>

<p>We need a method that doesn’t fear branching, doesn’t fear backtracking.</p>

<hr />

<h2 id="translating-clues-to-xcc">Translating Clues to XCC</h2>

<p>The previous two articles (<a href="/2026/03/dlx-colors.html">Colors and Stickers</a>, <a href="/2026/03/dlx-xcc.html">Crossword Puzzle</a>) introduced the core idea of XCC and its applications: <strong>primary items must be covered exactly once, secondary items can be covered multiple times but colors must be consistent</strong>. Secondary items represent “attribute slots at the same position”—multiple clues can constrain them simultaneously, and as long as they give the same color (attribute value), the algorithm won’t report a conflict.</p>

<p>The translation rules for the zebra puzzle are straightforward:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Concept</th>
      <th style="text-align: left">Role in XCC</th>
      <th style="text-align: left">Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left">Each clue</td>
      <td style="text-align: left"><strong>Primary item</strong></td>
      <td style="text-align: left">Each clue must be satisfied exactly once</td>
    </tr>
    <tr>
      <td style="text-align: left">Each house’s attribute slot</td>
      <td style="text-align: left"><strong>Secondary item</strong></td>
      <td style="text-align: left">\(N_j, J_j, P_j, D_j, C_j\) (N=nationality, J=job, P=pet, D=drink, C=color; \(j=0..4\))</td>
    </tr>
  </tbody>
</table>

<p>The color value of a secondary item is the specific attribute value. For example, “\(N_2\) has color England” means “house 2 is occupied by the Englishman.”</p>

<p>Generic CSP solvers can also handle this, but the XCC framework has advantages: secondary items natively support the semantic “multiple clues constrain the same slot,” and solving and puzzle generation use the same modeling pattern, calling the same solver—as we’ll see later.</p>

<h3 id="how-do-clues-become-options">How Do Clues Become Options?</h3>

<p>Take clue 1 (“The Englishman lives in the red house”) as an example. We don’t know which house the Englishman lives in, so we generate an option for each possibility:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#1  N0:England  C0:red     ← Englishman in house 0 (red)
#1  N1:England  C1:red     ← Englishman in house 1 (red)
#1  N2:England  C2:red     ← Englishman in house 2 (red)
#1  N3:England  C3:red     ← Englishman in house 3 (red)
#1  N4:England  C4:red     ← Englishman in house 4 (red)
</code></pre></div></div>

<p>Each option contains one primary item (<code class="language-plaintext highlighter-rouge">#1</code>, meaning “clue 1 is satisfied”) and several secondary items (meaning “these attribute slots are set to these values”).</p>

<p>The algorithm selects exactly one from these 5 options—once selected, the corresponding secondary items are “locked.” If a subsequent clue tries to color the same \(N_j\) a different color, the algorithm automatically backtracks.</p>

<h3 id="next-door-type-clues">“Next Door” Type Clues</h3>

<p>Clue 11 (“The horse lives next to the diplomat”) is slightly more complex. “Next door” means two possibilities: horse on the left, diplomat on the right, or vice versa. For each adjacent position \((i, i+1)\), we must enumerate both cases:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#11  P0:horse  J1:diplomat    ← Horse at 0, diplomat at 1
#11  J0:diplomat  P1:horse    ← Diplomat at 0, horse at 1
#11  P1:horse  J2:diplomat    ← Horse at 1, diplomat at 2
...
</code></pre></div></div>

<p>4 pairs of adjacent positions × 2 directions = 8 options.</p>

<h3 id="fixed-position-clues">Fixed Position Clues</h3>

<p>Clue 5 (“The Norwegian lives in the leftmost house”) is simplest—only one option:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#5  N0:Norway
</code></pre></div></div>

<h3 id="total">Total</h3>

<p>16 clues generate a total of <strong>80 options</strong>. Once built, call the <code class="language-plaintext highlighter-rouge">DLX_C</code> solver from the previous article to get the unique solution instantly.</p>

<hr />

<h2 id="code">Code</h2>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">dlx_colors</span> <span class="kn">import</span> <span class="n">DLX_C</span>

<span class="c1"># 16 primary items (clues), 25 secondary items (attribute slots)
</span><span class="n">num_primary</span> <span class="o">=</span> <span class="mi">16</span>
<span class="n">sec_base</span> <span class="o">=</span> <span class="mi">16</span>
<span class="k">def</span> <span class="nf">sec_idx</span><span class="p">(</span><span class="n">cat</span><span class="p">,</span> <span class="n">house</span><span class="p">):</span>  <span class="c1"># cat: 0=N,1=J,2=P,3=D,4=C
</span>    <span class="k">return</span> <span class="n">sec_base</span> <span class="o">+</span> <span class="n">cat</span> <span class="o">*</span> <span class="mi">5</span> <span class="o">+</span> <span class="n">house</span>

<span class="c1"># Clue 1: Englishman in red house (5 options, clue_id=0)
</span><span class="k">for</span> <span class="n">j</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">5</span><span class="p">):</span>
    <span class="n">add_option</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="sa">f</span><span class="s">"#1 N</span><span class="si">{</span><span class="n">j</span><span class="si">}</span><span class="s">:England C</span><span class="si">{</span><span class="n">j</span><span class="si">}</span><span class="s">:red"</span><span class="p">,</span>
               <span class="p">[(</span><span class="n">sec_idx</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">j</span><span class="p">),</span> <span class="s">'England'</span><span class="p">),</span> <span class="p">(</span><span class="n">sec_idx</span><span class="p">(</span><span class="mi">4</span><span class="p">,</span> <span class="n">j</span><span class="p">),</span> <span class="s">'red'</span><span class="p">)])</span>

<span class="c1"># Clue 9: White house immediately left of green (4 options, clue_id=8)
</span><span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">4</span><span class="p">):</span>
    <span class="n">add_option</span><span class="p">(</span><span class="mi">8</span><span class="p">,</span> <span class="sa">f</span><span class="s">"#9 C</span><span class="si">{</span><span class="n">i</span><span class="si">}</span><span class="s">:white C</span><span class="si">{</span><span class="n">i</span><span class="o">+</span><span class="mi">1</span><span class="si">}</span><span class="s">:green"</span><span class="p">,</span>
               <span class="p">[(</span><span class="n">sec_idx</span><span class="p">(</span><span class="mi">4</span><span class="p">,</span> <span class="n">i</span><span class="p">),</span> <span class="s">'white'</span><span class="p">),</span> <span class="p">(</span><span class="n">sec_idx</span><span class="p">(</span><span class="mi">4</span><span class="p">,</span> <span class="n">i</span><span class="o">+</span><span class="mi">1</span><span class="p">),</span> <span class="s">'green'</span><span class="p">)])</span>

<span class="c1"># Clue 11: Horse next to diplomat (8 options, clue_id=10)
</span><span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">4</span><span class="p">):</span>
    <span class="n">add_option</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span> <span class="sa">f</span><span class="s">"#11 P</span><span class="si">{</span><span class="n">i</span><span class="si">}</span><span class="s">:horse J</span><span class="si">{</span><span class="n">i</span><span class="o">+</span><span class="mi">1</span><span class="si">}</span><span class="s">:diplomat"</span><span class="p">,</span>
               <span class="p">[(</span><span class="n">sec_idx</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="n">i</span><span class="p">),</span> <span class="s">'horse'</span><span class="p">),</span> <span class="p">(</span><span class="n">sec_idx</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="n">i</span><span class="o">+</span><span class="mi">1</span><span class="p">),</span> <span class="s">'diplomat'</span><span class="p">)])</span>
    <span class="n">add_option</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span> <span class="sa">f</span><span class="s">"#11 J</span><span class="si">{</span><span class="n">i</span><span class="si">}</span><span class="s">:diplomat P</span><span class="si">{</span><span class="n">i</span><span class="o">+</span><span class="mi">1</span><span class="si">}</span><span class="s">:horse"</span><span class="p">,</span>
               <span class="p">[(</span><span class="n">sec_idx</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="n">i</span><span class="p">),</span> <span class="s">'diplomat'</span><span class="p">),</span> <span class="p">(</span><span class="n">sec_idx</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="n">i</span><span class="o">+</span><span class="mi">1</span><span class="p">),</span> <span class="s">'horse'</span><span class="p">)])</span>
</code></pre></div></div>

<p>See <a href="https://github.com/morefreeze/morefreeze.github.io/blob/master/code/zebra_puzzle.py">zebra_puzzle.py</a> for the complete implementation. Running it produces:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>       House       0       1       2       3       4
  --------------------------------------------------------
 Nationality  Norway Ukraine England   Spain   Japan
         Job diplomat   nurse sculptor violinist painter
         Pet     fox   horse  snails     dog   zebra
       Drink   water     tea    milk      oj  coffee
       Color  yellow    blue     red   white   green
</code></pre></div></div>

<p><strong>The Japanese owns the zebra, the Norwegian drinks water.</strong></p>

<p><img src="/images/zebra-puzzle-solution.svg" alt="Zebra Puzzle Solution" /></p>

<hr />

<h2 id="reversing-it-automatic-puzzle-generation">Reversing It: Automatic Puzzle Generation</h2>

<p>Solving is “given clues, find the assignment.” Reversing it—<strong>given an assignment, find the minimum clues that make the solution unique</strong>—is automatic puzzle generation.</p>

<h3 id="algorithm">Algorithm</h3>

<ol>
  <li><strong>Randomly generate a valid assignment</strong>: randomly permute 5 values from each category into 5 houses</li>
  <li><strong>Enumerate all possible clues</strong>:
    <ul>
      <li>Same-house clues: \(\binom{5}{2} \times 5 = 50\) (“attribute A and B are in the same house”)</li>
      <li>Adjacent-house clues: \(\binom{5}{2} \times 4 \times 2 + 5 \times 4 = 100\) (“attribute A and B are in adjacent houses”)</li>
      <li>Fixed-position clues: \(5 \times 5 = 25\) (“attribute A is in house k”)</li>
    </ul>
  </li>
  <li><strong>Greedy removal</strong>: shuffle clue order, try removing each one. If removal still yields a unique solution (verified by XCC), remove it; otherwise keep it</li>
</ol>

<p>This doesn’t guarantee a globally minimal clue set (that’s NP-hard), but works well in practice—usually compressing to 15-20 clues.</p>

<h3 id="an-example">An Example</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">clues</span><span class="p">,</span> <span class="n">answer</span> <span class="o">=</span> <span class="n">generate_puzzle</span><span class="p">(</span><span class="n">seed</span><span class="o">=</span><span class="mi">42</span><span class="p">)</span>
</code></pre></div></div>

<p>Output:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> 1. The person with Nationality=Canadian also has Color=green.
 2. The person with Drink=coffee also has Color=blue.
 3. The person with Nationality=French lives next to the person with Drink=soda.
 4. The person with Job=lawyer also has Color=yellow.
 5. The person with Job=teacher lives next to the person with Job=artist.
 6. The person with Job=artist also has Pet=cat.
 7. The person with Job=lawyer also has Drink=juice.
 8. The person with Nationality=British lives next to the person with Job=chef.
 9. The person with Pet=hamster also has Drink=coffee.
10. The person with Drink=water lives in the fourth house.
11. The person with Job=lawyer lives next to the person with Color=green.
12. The person with Pet=bird also has Color=white.
13. The person with Nationality=British also has Job=lawyer.
14. The person with Nationality=Dutch lives in the first house.
15. The person with Pet=bird lives next to the person with Pet=dog.
16. The person with Nationality=British lives next to the person with Job=teacher.
17. The person with Drink=juice lives in the second house.
</code></pre></div></div>

<p>17 clues, unique solution. Use it to test your friends.</p>

<h3 id="key-code">Key Code</h3>

<p>Puzzle generation and solving use the same XCC modeling pattern. The only difference: when generating puzzles, we need additional <strong>all-different</strong> primary items—because now there aren’t enough clues, the algorithm needs to explicitly know “each attribute value appears exactly once”:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Each attribute value corresponds to a primary item, 5 options (which house)
</span><span class="k">for</span> <span class="n">cat_i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">num_cats</span><span class="p">):</span>
    <span class="k">for</span> <span class="n">v_i</span><span class="p">,</span> <span class="n">val</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">values</span><span class="p">[</span><span class="n">cat_i</span><span class="p">]):</span>
        <span class="k">for</span> <span class="n">j</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">n</span><span class="p">):</span>
            <span class="n">options</span><span class="p">.</span><span class="n">append</span><span class="p">([</span>
                <span class="p">(</span><span class="n">ad_idx</span><span class="p">(</span><span class="n">cat_i</span><span class="p">,</span> <span class="n">v_i</span><span class="p">),</span> <span class="bp">None</span><span class="p">),</span>   <span class="c1"># primary: this value must be assigned
</span>                <span class="p">(</span><span class="n">sec_idx</span><span class="p">(</span><span class="n">cat_i</span><span class="p">,</span> <span class="n">j</span><span class="p">),</span> <span class="n">val</span><span class="p">)</span>       <span class="c1"># secondary: put in house j
</span>            <span class="p">])</span>
</code></pre></div></div>

<p>This is exactly the <strong>CSP → XCC general translation</strong> Knuth discusses in Exercise 100(c), with the zebra puzzle (Exercise 101) as its classic application: each variable’s each value is an option, constraints are automatically guaranteed through secondary item color consistency.</p>

<hr />

<h2 id="knuths-small-optimization">Knuth’s Small Optimization</h2>

<p>Knuth mentions a trick in his TAOCP 4B answers: the modeling above <strong>does not</strong> explicitly constrain “each attribute value can only appear in one house.” For example, the algorithm doesn’t directly know “if England is in house 2, it can’t appear in other houses”—it just happens to derive this from the cross-constraints of the 16 clues.</p>

<p>If we add 25 additional <strong>inverse mapping</strong> secondary items (one per attribute value, color = house number), letting the algorithm detect conflicts earlier, the search tree shrinks from <strong>112 nodes</strong> to <strong>32 nodes</strong>. A few lines of code, but the time saved “just offsets” the overhead of these extra items—Knuth’s words.</p>

<hr />

<h2 id="recap">Recap</h2>

<p>This is the third in the series. The first two are <a href="/2026/03/dlx-colors.html">Colors and Stickers</a> and <a href="/2026/03/dlx-xcc.html">Crossword Puzzle</a>—recommended reading in order.</p>

<p>Four articles used the same solver (<code class="language-plaintext highlighter-rouge">DLX_C</code>) to solve four completely different problems:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Article</th>
      <th style="text-align: left">Problem</th>
      <th style="text-align: left">Primary Items</th>
      <th style="text-align: left">Secondary Items</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><a href="/2026/03/dlx-colors.html">Colors and Stickers</a></td>
      <td style="text-align: left">Color item bug and fix</td>
      <td style="text-align: left">Each word</td>
      <td style="text-align: left">Each cell (color = letter)</td>
    </tr>
    <tr>
      <td style="text-align: left"><a href="/2026/03/dlx-xcc.html">Crossword Puzzle</a></td>
      <td style="text-align: left">Packing words into grid</td>
      <td style="text-align: left">Each word</td>
      <td style="text-align: left">Each cell (color = letter)</td>
    </tr>
    <tr>
      <td style="text-align: left">This article (solving)</td>
      <td style="text-align: left">Satisfy logic clues</td>
      <td style="text-align: left">Each clue</td>
      <td style="text-align: left">Each attribute slot (color = attribute value)</td>
    </tr>
    <tr>
      <td style="text-align: left">This article (generating)</td>
      <td style="text-align: left">Find minimum clues</td>
      <td style="text-align: left">Clues + all-different</td>
      <td style="text-align: left">Same as above</td>
    </tr>
  </tbody>
</table>

<p>Four seemingly unrelated problems share the same underlying structure: describe “what constitutes a valid choice” and let the algorithm find the set that covers everything.</p>

<p>We never wrote any code about “how to reason.” Each time, we just described “what constitutes a valid answer” in a different way, and the algorithm automatically found all answers.</p>

<p>This is the most fascinating thing about XCC: <strong>you describe the problem, it solves it.</strong></p>

<hr />

<h2 id="complete-code">Complete Code</h2>

<p><a href="https://github.com/morefreeze/morefreeze.github.io/blob/master/code/zebra_puzzle.py">zebra_puzzle.py</a> contains both solving and puzzle generation, depending on <a href="https://github.com/morefreeze/morefreeze.github.io/blob/master/code/dlx_colors.py">dlx_colors.py</a>. Just run it directly.</p>

<p>Change the random seed to generate a new puzzle—use it to test your friends.</p>]]></content><author><name>More Freeze</name></author><category term="algorithm" /><category term="knuth" /><category term="exact-cover" /><category term="dancing-links" /><category term="CSP" /><category term="zebra-puzzle" /><category term="TAOCP" /><summary type="html"><![CDATA[The classic Einstein riddle can drive you crazy with manual table-drawing. Translate it to an XCC problem and the algorithm finds the unique solution in under a millisecond—then we reverse it to let the algorithm automatically generate new puzzles.]]></summary></entry><entry><title type="html">程序是怎么自动出拼字谜题的？</title><link href="https://blog.morefreeze.top/2026/03/dlx-xcc.html" rel="alternate" type="text/html" title="程序是怎么自动出拼字谜题的？" /><published>2026-03-23T00:00:00+00:00</published><updated>2026-03-23T00:00:00+00:00</updated><id>https://blog.morefreeze.top/2026/03/dlx-xcc</id><content type="html" xml:base="https://blog.morefreeze.top/2026/03/dlx-xcc.html"><![CDATA[<p>你一定见过这种东西：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>C A N T O R
G A U S S .
E U L E R .
A B E L . .
. . . . . .
. . . . . .
</code></pre></div></div>

<p>一个字母方格，藏着若干单词——横着、竖着，有时候斜着。找到它们是游戏，但<strong>出这道题</strong>才是有趣的工程问题：给你几个单词，怎么把它们都塞进格子里？</p>

<p>这件事比看起来难。单词可以斜放，可以反向，两个单词可能在某个格子相交——相交处的字母必须相同。暴力枚举所有摆法代价太大，还容易遗漏。</p>

<p>这篇文章介绍一种优雅的做法：把”出谜题”变成一个<strong>填色游戏</strong>，然后用舞蹈链算法自动求解。顺带揭露一个很容易写错的细节——以及 Knuth 用来修复它的一个小技巧。</p>

<!--more-->

<hr />

<h2 id="先把问题说清楚">先把问题说清楚</h2>

<p>给你 4 个词：ABEL、CANTOR、GAUSS、EULER，一个 6×6 的格子，要求：</p>
<ul>
  <li>每个词恰好放一次</li>
  <li>可以横放、竖放、斜放（8 个方向）</li>
  <li>如果两个词在某格相交，该格字母必须相同</li>
</ul>

<p>问：有多少种合法的摆法？</p>

<p>答案是 <strong>244,792 种</strong>。</p>

<p>手算？不可能。暴力枚举？太慢。这篇文章介绍的方法几秒钟就算完了。</p>

<hr />

<h2 id="把摆词变成涂色">把”摆词”变成”涂色”</h2>

<p>关键洞察：我们可以把这道题重新描述为一个<strong>涂色问题</strong>。</p>

<p>每个格子想象成一个空白格，可以被涂上某个字母的颜色。规则很简单：</p>

<blockquote>
  <p>同一个格子可以被涂色多次，<strong>但所有涂色必须用同一种颜色</strong>。</p>
</blockquote>

<p>每次”把一个词以某个方向放在某个位置”就是一个<strong>涂色方案</strong>——它会把经过的每个格子涂成对应的字母。</p>

<p>比如”ABEL 从 (0,0) 向东放置”这个方案，会把：</p>
<ul>
  <li>格子 (0,0) 涂成 <strong>A</strong></li>
  <li>格子 (0,1) 涂成 <strong>B</strong></li>
  <li>格子 (0,2) 涂成 <strong>E</strong></li>
  <li>格子 (0,3) 涂成 <strong>L</strong></li>
</ul>

<p>如果另一个方案也经过格子 (0,3)，它必须也涂 <strong>L</strong>，否则就冲突了。</p>

<p>这种”可以重复但颜色必须一致”的约束，正是 Knuth 在 TAOCP 4B 里引入的”颜色项”扩展。简单来说：颜色项允许同一个格子被多次覆盖，但要求每次覆盖携带的颜色（这里就是字母）必须相同。<a href="/2026/03/dlx-colors.html">上一篇</a>介绍了它的基本原理；这篇文章要解决一个用颜色项时几乎所有人都会踩到的坑。</p>

<hr />

<h2 id="两个词共享一个格子看起来对其实错了">两个词共享一个格子——看起来对，其实错了</h2>

<p>来看最小的反例。把 ABEL 和 LAND 放进 4×4 的格子，它们可以在字母 L 处相交：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>A B E L
. . . A
. . . N
. . . D
</code></pre></div></div>

<p>ABEL 横放第 0 行，LAND 从格子 (0,3) 向下竖放。两个词都经过格子 (0,3)，字母都是 L——合法！</p>

<p>用颜色项写出这道题，算法搜索时大概是这样的：</p>

<ol>
  <li>选”ABEL 横放第 0 行”这个方案</li>
  <li>把格子 (0,3) 涂成 L</li>
  <li>净化：扫描格子 (0,3) 这一列的所有其他候选方案，<strong>删掉那些想把 (0,3) 涂成其他颜色的方案</strong></li>
  <li>继续选”LAND 从 (0,3) 向南放置”</li>
</ol>

<p>问题出在第 4 步之后：LAND 向南这个方案也经过格子 (0,3)，它也需要”净化”格子 (0,3)——<strong>再次扫描这一列，再次删掉那些颜色不对的候选</strong>。</p>

<p>但有些候选早在第 3 步就被删过一次了！</p>

<p><strong>删了两次会怎样？</strong></p>

<p>想象格子 (0,3) 这一列里有一个候选方案 X，颜色是 D（不是 L）。第 3 步删掉它，同时把这列的”剩余可用候选数”减 1。第 4 步再删一次，计数再减 1。</p>

<p>回溯时，两次删除只被恢复一次，计数器就乱掉了。某些列明明还有可用方案，计数器却说”没了”——算法错误地以为走入了死胡同，或者走错了方向，最终给出错误的答案。</p>

<p>如果我们只考虑横向和纵向（不含对角线），并且要求两个词至少在一个格子上交叉，正确答案是 <strong>16 个</strong>。但用有 bug 的版本来跑，程序会给出 <strong>28 个解</strong>。多出来的 12 个全是错的，但程序不会报错，只会静默地给你假答案。</p>

<hr />

<h2 id="贴纸修复法">贴纸修复法</h2>

<p>这个 bug 的根源是：<strong>第一次净化没有留下任何痕迹</strong>，第二次净化以为自己是第一个操作者。</p>

<p>Knuth 的修复方案非常简单：让第一次净化留下一个<strong>贴纸</strong>。</p>

<blockquote>
  <p><strong>规则</strong>：净化操作扫描格子列时，遇到<strong>颜色相同</strong>的候选，不是跳过，而是<strong>贴上”已处理”标签</strong>（书里叫 COMMITTED）。删除操作遇到有”已处理”标签的节点，直接跳过，什么都不做。</p>
</blockquote>

<p>效果：</p>

<ul>
  <li>第一次净化（来自 ABEL 的方案）把格子 (0,3) 里某个颜色是 L 的候选贴上标签</li>
  <li>第二次净化（来自 LAND 的方案）扫描到这个候选，看到标签，<strong>跳过</strong>，不再重复操作</li>
</ul>

<p>回溯时，逆向扫描，把标签揭掉，恢复成原来的颜色——一切如初。</p>

<p>这个贴纸机制有个细节很精妙：<strong>当前选中方案本身的节点不会被贴上标签</strong>。因为在净化开始之前，选中方案已经被”提取”出了列链（用于其他操作），净化扫描列时根本找不到它。这样，回溯时这个节点的颜色始终是原始值，算法才能正确判断”该调用哪个恢复操作”。</p>

<hr />

<h2 id="代码">代码</h2>

<p>加了贴纸机制的关键代码（完整实现见 <a href="https://github.com/morefreeze/morefreeze.github.io/blob/master/code/dlx_colors.py">dlx_colors.py</a>）：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">_COMMITTED</span> <span class="o">=</span> <span class="nb">object</span><span class="p">()</span>   <span class="c1"># 贴纸：已被外层净化处理过
</span>
<span class="k">def</span> <span class="nf">_hide</span><span class="p">(</span><span class="n">row_node</span><span class="p">):</span>
    <span class="s">"""删除某行时，有贴纸的节点跳过"""</span>
    <span class="n">j</span> <span class="o">=</span> <span class="n">row_node</span><span class="p">.</span><span class="n">R</span>
    <span class="k">while</span> <span class="n">j</span> <span class="ow">is</span> <span class="ow">not</span> <span class="n">row_node</span><span class="p">:</span>
        <span class="k">if</span> <span class="n">j</span><span class="p">.</span><span class="n">color</span> <span class="ow">is</span> <span class="ow">not</span> <span class="n">_COMMITTED</span><span class="p">:</span>   <span class="c1"># 有贴纸？跳过
</span>            <span class="n">j</span><span class="p">.</span><span class="n">D</span><span class="p">.</span><span class="n">U</span> <span class="o">=</span> <span class="n">j</span><span class="p">.</span><span class="n">U</span>
            <span class="n">j</span><span class="p">.</span><span class="n">U</span><span class="p">.</span><span class="n">D</span> <span class="o">=</span> <span class="n">j</span><span class="p">.</span><span class="n">D</span>
            <span class="n">j</span><span class="p">.</span><span class="n">C</span><span class="p">.</span><span class="n">S</span> <span class="o">-=</span> <span class="mi">1</span>
        <span class="n">j</span> <span class="o">=</span> <span class="n">j</span><span class="p">.</span><span class="n">R</span>

<span class="k">def</span> <span class="nf">_purify</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">node</span><span class="p">):</span>
    <span class="s">"""净化：同色贴标签，异色删除"""</span>
    <span class="n">col</span><span class="p">,</span> <span class="n">color</span> <span class="o">=</span> <span class="n">node</span><span class="p">.</span><span class="n">C</span><span class="p">,</span> <span class="n">node</span><span class="p">.</span><span class="n">color</span>
    <span class="n">i</span> <span class="o">=</span> <span class="n">col</span><span class="p">.</span><span class="n">D</span>
    <span class="k">while</span> <span class="n">i</span> <span class="ow">is</span> <span class="ow">not</span> <span class="n">col</span><span class="p">:</span>
        <span class="k">if</span> <span class="n">i</span><span class="p">.</span><span class="n">color</span> <span class="o">==</span> <span class="n">color</span><span class="p">:</span>
            <span class="n">i</span><span class="p">.</span><span class="n">color</span> <span class="o">=</span> <span class="n">_COMMITTED</span>    <span class="c1"># 同色：贴标签
</span>        <span class="k">else</span><span class="p">:</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">_hide</span><span class="p">(</span><span class="n">i</span><span class="p">)</span>           <span class="c1"># 异色：删掉
</span>        <span class="n">i</span> <span class="o">=</span> <span class="n">i</span><span class="p">.</span><span class="n">D</span>

<span class="k">def</span> <span class="nf">_unpurify</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">node</span><span class="p">):</span>
    <span class="s">"""回溯：逆序揭标签或恢复被删的行"""</span>
    <span class="n">col</span><span class="p">,</span> <span class="n">color</span> <span class="o">=</span> <span class="n">node</span><span class="p">.</span><span class="n">C</span><span class="p">,</span> <span class="n">node</span><span class="p">.</span><span class="n">color</span>
    <span class="n">i</span> <span class="o">=</span> <span class="n">col</span><span class="p">.</span><span class="n">U</span>                       <span class="c1"># 注意逆序
</span>    <span class="k">while</span> <span class="n">i</span> <span class="ow">is</span> <span class="ow">not</span> <span class="n">col</span><span class="p">:</span>
        <span class="k">if</span> <span class="n">i</span><span class="p">.</span><span class="n">color</span> <span class="ow">is</span> <span class="n">_COMMITTED</span><span class="p">:</span>
            <span class="n">i</span><span class="p">.</span><span class="n">color</span> <span class="o">=</span> <span class="n">color</span>         <span class="c1"># 揭标签，恢复原色
</span>        <span class="k">else</span><span class="p">:</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">_unhide</span><span class="p">(</span><span class="n">i</span><span class="p">)</span>         <span class="c1"># 恢复被删的行
</span>        <span class="n">i</span> <span class="o">=</span> <span class="n">i</span><span class="p">.</span><span class="n">U</span>
</code></pre></div></div>

<hr />

<h2 id="拼字谜题的完整建模">拼字谜题的完整建模</h2>

<p>有了这个机制，把拼字谜题翻译成算法输入就很直接了：</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">概念</th>
      <th style="text-align: left">对应的”列”</th>
      <th style="text-align: left">说明</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left">每个单词</td>
      <td style="text-align: left">主要列</td>
      <td style="text-align: left">必须恰好放一次</td>
    </tr>
    <tr>
      <td style="text-align: left">每个格子</td>
      <td style="text-align: left">颜色列</td>
      <td style="text-align: left">可以放多次，但颜色（字母）必须一致</td>
    </tr>
  </tbody>
</table>

<p>每个”摆法”（单词 + 位置 + 方向）对应一行，覆盖：</p>
<ul>
  <li>这个单词对应的主要列（说明”这个词已放好”）</li>
  <li>经过的每个格子的颜色列，颜色 = 对应字母</li>
</ul>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">create_word_search</span><span class="p">(</span><span class="n">rows</span><span class="p">,</span> <span class="n">cols</span><span class="p">,</span> <span class="n">words</span><span class="p">,</span> <span class="n">allow_diagonal</span><span class="o">=</span><span class="bp">True</span><span class="p">):</span>
    <span class="n">W</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">words</span><span class="p">)</span>
    <span class="n">num_primary</span> <span class="o">=</span> <span class="n">W</span>                  <span class="c1"># 前 W 列是主要列（单词）
</span>    <span class="n">num_items</span>   <span class="o">=</span> <span class="n">W</span> <span class="o">+</span> <span class="n">rows</span> <span class="o">*</span> <span class="n">cols</span>    <span class="c1"># 后面每格一列（颜色列）
</span>
    <span class="n">options</span><span class="p">,</span> <span class="n">labels</span> <span class="o">=</span> <span class="p">[],</span> <span class="p">[]</span>

    <span class="k">for</span> <span class="n">w_idx</span><span class="p">,</span> <span class="n">word</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">words</span><span class="p">):</span>
        <span class="k">for</span> <span class="n">dir_name</span><span class="p">,</span> <span class="p">(</span><span class="n">dr</span><span class="p">,</span> <span class="n">dc</span><span class="p">)</span> <span class="ow">in</span> <span class="n">DIRECTIONS</span><span class="p">.</span><span class="n">items</span><span class="p">():</span>
            <span class="k">for</span> <span class="n">sr</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">rows</span><span class="p">):</span>
                <span class="k">for</span> <span class="n">sc</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">cols</span><span class="p">):</span>
                    <span class="c1"># 越界检查
</span>                    <span class="n">er</span> <span class="o">=</span> <span class="n">sr</span> <span class="o">+</span> <span class="n">dr</span> <span class="o">*</span> <span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">word</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span>
                    <span class="n">ec</span> <span class="o">=</span> <span class="n">sc</span> <span class="o">+</span> <span class="n">dc</span> <span class="o">*</span> <span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">word</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span>
                    <span class="k">if</span> <span class="ow">not</span> <span class="p">(</span><span class="mi">0</span> <span class="o">&lt;=</span> <span class="n">er</span> <span class="o">&lt;</span> <span class="n">rows</span> <span class="ow">and</span> <span class="mi">0</span> <span class="o">&lt;=</span> <span class="n">ec</span> <span class="o">&lt;</span> <span class="n">cols</span><span class="p">):</span>
                        <span class="k">continue</span>
                    <span class="c1"># 构造这一行：主要项 + 每个字母的颜色项
</span>                    <span class="n">option</span> <span class="o">=</span> <span class="p">[(</span><span class="n">w_idx</span><span class="p">,</span> <span class="bp">None</span><span class="p">)]</span>
                    <span class="k">for</span> <span class="n">k</span><span class="p">,</span> <span class="n">ch</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">word</span><span class="p">):</span>
                        <span class="n">r</span><span class="p">,</span> <span class="n">c</span> <span class="o">=</span> <span class="n">sr</span> <span class="o">+</span> <span class="n">dr</span><span class="o">*</span><span class="n">k</span><span class="p">,</span> <span class="n">sc</span> <span class="o">+</span> <span class="n">dc</span><span class="o">*</span><span class="n">k</span>
                        <span class="n">option</span><span class="p">.</span><span class="n">append</span><span class="p">((</span><span class="n">W</span> <span class="o">+</span> <span class="n">r</span><span class="o">*</span><span class="n">cols</span> <span class="o">+</span> <span class="n">c</span><span class="p">,</span> <span class="n">ch</span><span class="p">))</span>
                    <span class="n">options</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">option</span><span class="p">)</span>
                    <span class="n">labels</span><span class="p">.</span><span class="n">append</span><span class="p">((</span><span class="n">w_idx</span><span class="p">,</span> <span class="n">sr</span><span class="p">,</span> <span class="n">sc</span><span class="p">,</span> <span class="n">dir_name</span><span class="p">))</span>

    <span class="n">dlx</span> <span class="o">=</span> <span class="n">DLX_C</span><span class="p">(</span><span class="n">num_primary</span><span class="p">,</span> <span class="n">num_items</span><span class="p">,</span> <span class="n">options</span><span class="p">)</span>
    <span class="n">solutions</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">sol</span> <span class="ow">in</span> <span class="n">dlx</span><span class="p">.</span><span class="n">solve</span><span class="p">():</span>
        <span class="n">placement</span> <span class="o">=</span> <span class="p">[(</span><span class="n">words</span><span class="p">[</span><span class="n">labels</span><span class="p">[</span><span class="n">i</span><span class="p">][</span><span class="mi">0</span><span class="p">]],</span> <span class="o">*</span><span class="n">labels</span><span class="p">[</span><span class="n">i</span><span class="p">][</span><span class="mi">1</span><span class="p">:])</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="n">sol</span><span class="p">]</span>
        <span class="n">solutions</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">placement</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">solutions</span>
</code></pre></div></div>

<p>运行一下：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">words</span> <span class="o">=</span> <span class="p">[</span><span class="s">"ABEL"</span><span class="p">,</span> <span class="s">"CANTOR"</span><span class="p">,</span> <span class="s">"GAUSS"</span><span class="p">,</span> <span class="s">"EULER"</span><span class="p">]</span>
<span class="n">solutions</span> <span class="o">=</span> <span class="n">create_word_search</span><span class="p">(</span><span class="mi">6</span><span class="p">,</span> <span class="mi">6</span><span class="p">,</span> <span class="n">words</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">solutions</span><span class="p">))</span>   <span class="c1"># → 244792
</span></code></pre></div></div>

<p>第一个解（恰好四词各占一行）：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>CANTOR
GAUSS.
EULER.
ABEL..
......
......
</code></pre></div></div>

<p>更有趣的解包含斜向放置和字母交叉，都由算法自动处理，代码完全不需要为这些情况特殊处理。</p>

<hr />

<h2 id="词语矩形顺带解决另一个问题">词语矩形：顺带解决另一个问题</h2>

<p>同样的框架，换个约束，就能解另一个谜题——<strong>词语矩形</strong>：横向单词填满每行，纵向单词填满每列，交叉格的字母必须相同。</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>C A T
D O G
E E L
</code></pre></div></div>

<p>横向：CAT、DOG、EEL；纵向：CDE、AOE、TGL。</p>

<p>建模完全类似：行和列各是一个主要项，格子是颜色项。区别在于主要项从”每个单词”变成了”每行和每列”——行和列各有一个主要项，表示这行（列）必须恰好填入一个合法单词。改几行代码就行，底层算法不需要动。</p>

<hr />

<h2 id="换个角度看这件事">换个角度看这件事</h2>

<p>回顾一下我们做了什么：</p>

<ol>
  <li>把”拼字谜题”描述为”每个单词必须恰好放一次，每个格子的字母必须一致”</li>
  <li>把”字母一致”翻译成”颜色相同可以多次覆盖”</li>
  <li>发现朴素实现有双重操作的 bug</li>
  <li>用一个贴纸（COMMITTED 标签）修复它</li>
</ol>

<p>整个过程最有意思的地方是：<strong>我们没有写任何”如何解谜题”的逻辑</strong>。我们只是描述了”什么是合法的谜题”，算法自己找出了所有答案。</p>

<p>这是这类搜索算法最吸引人的特质：把约束描述清楚，求解自动发生。出谜题、解谜题，用的是同一套框架。</p>

<hr />

<h2 id="你来试试">你来试试？</h2>

<p>完整代码在 <a href="https://github.com/morefreeze/morefreeze.github.io/blob/master/code/dlx_colors.py">dlx_colors.py</a> 和 <a href="https://github.com/morefreeze/morefreeze.github.io/blob/master/code/word_search.py">word_search.py</a>，直接 clone 下来就能跑。</p>

<p>几个可以玩的方向：</p>
<ul>
  <li><strong>换一组词</strong>：把自己的名字或者感兴趣的词塞进去，看看有多少种藏法</li>
  <li><strong>改变格子大小</strong>：同样的词，6×6 和 8×8 的解的数量差异有多大？</li>
  <li><strong>加一个约束</strong>：如果要求所有词必须相交（每个词至少和另一个词共享一个格子），怎么修改建模？</li>
</ul>

<p>欢迎在评论区留下你的发现。</p>]]></content><author><name>More Freeze</name></author><category term="algorithm" /><category term="knuth" /><category term="exact-cover" /><category term="dancing-links" /><category term="word-search" /><category term="TAOCP" /><summary type="html"><![CDATA[从报纸上的找词游戏出发，聊聊一个藏在算法里的微妙 bug——以及 Knuth 用一个贴纸修复它的方法]]></summary></entry><entry><title type="html">How Does a Program Automatically Generate Word Search Puzzles?</title><link href="https://blog.morefreeze.top/2026/03/dlx-xcc_en.html" rel="alternate" type="text/html" title="How Does a Program Automatically Generate Word Search Puzzles?" /><published>2026-03-23T00:00:00+00:00</published><updated>2026-03-23T00:00:00+00:00</updated><id>https://blog.morefreeze.top/2026/03/dlx-xcc_en</id><content type="html" xml:base="https://blog.morefreeze.top/2026/03/dlx-xcc_en.html"><![CDATA[<p>You’ve definitely seen one of these before:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>C A N T O R
G A U S S .
E U L E R .
A B E L . .
. . . . . .
. . . . . .
</code></pre></div></div>

<p>A grid of letters hiding several words — horizontally, vertically, sometimes diagonally. Finding them is the game, but <strong>creating the puzzle</strong> is the interesting engineering problem: given a list of words, how do you fit them all into a grid?</p>

<p>This is harder than it looks. Words can go diagonally, they can go backwards, and two words might intersect at the same cell — where the letters must match. Brute-forcing every possible placement is too expensive and easy to get wrong.</p>

<p>This article introduces an elegant approach: turn “puzzle construction” into a <strong>coloring game</strong>, then let Dancing Links solve it automatically. Along the way, we’ll expose a detail that’s surprisingly easy to get wrong — and a little trick Knuth uses to fix it.</p>

<!--more-->

<hr />

<h2 id="lets-define-the-problem">Let’s Define the Problem</h2>

<p>Given 4 words — ABEL, CANTOR, GAUSS, EULER — and a 6x6 grid, the rules are:</p>
<ul>
  <li>Each word must be placed exactly once</li>
  <li>Words can go horizontally, vertically, or diagonally (8 directions)</li>
  <li>If two words intersect at a cell, that cell’s letter must be the same for both</li>
</ul>

<p>Question: how many valid placements are there?</p>

<p>The answer is <strong>244,792</strong>.</p>

<p>By hand? Impossible. Brute force? Too slow. The method in this article computes it in seconds.</p>

<hr />

<h2 id="turning-word-placement-into-coloring">Turning “Word Placement” into “Coloring”</h2>

<p>The key insight: we can reformulate this problem as a <strong>coloring problem</strong>.</p>

<p>Imagine each cell as a blank square that can be painted with a letter’s color. The rule is simple:</p>

<blockquote>
  <p>A cell can be colored multiple times, <strong>but every coloring must use the same color</strong>.</p>
</blockquote>

<p>Each “place a word at some position in some direction” is a <strong>coloring option</strong> — it paints each cell it passes through with the corresponding letter.</p>

<p>For example, the option “place ABEL starting at (0,0) going east” paints:</p>
<ul>
  <li>Cell (0,0) with <strong>A</strong></li>
  <li>Cell (0,1) with <strong>B</strong></li>
  <li>Cell (0,2) with <strong>E</strong></li>
  <li>Cell (0,3) with <strong>L</strong></li>
</ul>

<p>If another option also passes through cell (0,3), it must also paint it <strong>L</strong>, otherwise there’s a conflict.</p>

<p>This “repeatable but must be the same color” constraint is exactly the “color item” extension that Knuth introduced in TAOCP 4B. In short: a color item can be covered multiple times, as long as every covering carries the same color (here, the same letter). The <a href="/2026/03/dlx-colors_en.html">previous post</a> covered the basics; this article tackles a pitfall that almost everyone falls into when using color items.</p>

<hr />

<h2 id="two-words-sharing-a-cell--looks-right-actually-wrong">Two Words Sharing a Cell — Looks Right, Actually Wrong</h2>

<p>Let’s look at the smallest counterexample. Place ABEL and LAND in a 4x4 grid. They can intersect at the letter L:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>A B E L
. . . A
. . . N
. . . D
</code></pre></div></div>

<p>ABEL goes across row 0, LAND goes down from cell (0,3). Both words pass through cell (0,3), both with the letter L — valid!</p>

<p>When we express this with color items, here’s roughly what the algorithm does during search:</p>

<ol>
  <li>Choose the option “ABEL across row 0”</li>
  <li>Color cell (0,3) with L</li>
  <li>Purify: scan all other candidates in cell (0,3)’s column, <strong>remove those that want to color (0,3) a different color</strong></li>
  <li>Next, choose “LAND going south from (0,3)”</li>
</ol>

<p>The problem comes after step 4: the LAND-going-south option also passes through cell (0,3), so it also needs to “purify” cell (0,3) — <strong>scanning the column again, removing candidates with the wrong color again</strong>.</p>

<p>But some candidates were already removed in step 3!</p>

<p><strong>What happens when you remove something twice?</strong></p>

<p>Imagine there’s a candidate option X in cell (0,3)’s column with color D (not L). Step 3 removes it and decrements the column’s “remaining available candidates” count by 1. Step 4 removes it again, decrementing the count by another 1.</p>

<p>During backtracking, the two removals are only undone once, so the counter gets corrupted. Some columns still have available options, but the counter says “none left” — the algorithm incorrectly thinks it’s hit a dead end or takes a wrong turn, ultimately producing wrong answers.</p>

<p>If we restrict to horizontal and vertical directions only (no diagonals) and require that the two words share at least one cell, the correct answer is <strong>16 solutions</strong>. But the buggy version reports <strong>28</strong>. The extra 12 are all wrong, but the program won’t raise an error — it silently gives you bogus answers.</p>

<p>Running the buggy version on the ABEL+LAND puzzle confirms this: the program reports <strong>28 solutions</strong> instead of the correct <strong>16</strong>.</p>

<hr />

<h2 id="the-sticker-fix">The Sticker Fix</h2>

<p>The root cause of this bug: <strong>the first purification leaves no trace</strong>, so the second purification thinks it’s the first one to operate.</p>

<p>Knuth’s fix is remarkably simple: make the first purification leave a <strong>sticker</strong>.</p>

<blockquote>
  <p><strong>Rule</strong>: When a purify operation scans a column, candidates with <strong>the same color</strong> aren’t just skipped — they get <strong>tagged as “already processed”</strong> (called COMMITTED in the book). When a hide operation encounters a node with the “already processed” tag, it simply skips it and does nothing.</p>
</blockquote>

<p>The effect:</p>

<ul>
  <li>The first purification (from ABEL’s option) tags a candidate in cell (0,3) with color L</li>
  <li>The second purification (from LAND’s option) scans to this candidate, sees the tag, <strong>skips it</strong>, no duplicate operation</li>
</ul>

<p>During backtracking, the reverse scan peels off the tags and restores the original colors — everything back to normal.</p>

<p>There’s a subtle elegance to this sticker mechanism: <strong>the currently selected option’s own nodes never get tagged</strong>. Because before purification begins, the selected option has already been “extracted” from the column link (for other operations), so the purification scan can’t find it. This way, the node’s color always remains the original value during backtracking, which is how the algorithm correctly determines “which restore operation to call.”</p>

<hr />

<h2 id="the-code">The Code</h2>

<p>Here’s the key code with the sticker mechanism (full implementation at <a href="https://github.com/morefreeze/morefreeze.github.io/blob/master/code/dlx_colors.py">dlx_colors.py</a>):</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">_COMMITTED</span> <span class="o">=</span> <span class="nb">object</span><span class="p">()</span>   <span class="c1"># Sticker: already processed by outer purification
</span>
<span class="k">def</span> <span class="nf">_hide</span><span class="p">(</span><span class="n">row_node</span><span class="p">):</span>
    <span class="s">"""When hiding a row, skip nodes with stickers"""</span>
    <span class="n">j</span> <span class="o">=</span> <span class="n">row_node</span><span class="p">.</span><span class="n">R</span>
    <span class="k">while</span> <span class="n">j</span> <span class="ow">is</span> <span class="ow">not</span> <span class="n">row_node</span><span class="p">:</span>
        <span class="k">if</span> <span class="n">j</span><span class="p">.</span><span class="n">color</span> <span class="ow">is</span> <span class="ow">not</span> <span class="n">_COMMITTED</span><span class="p">:</span>   <span class="c1"># Has a sticker? Skip
</span>            <span class="n">j</span><span class="p">.</span><span class="n">D</span><span class="p">.</span><span class="n">U</span> <span class="o">=</span> <span class="n">j</span><span class="p">.</span><span class="n">U</span>
            <span class="n">j</span><span class="p">.</span><span class="n">U</span><span class="p">.</span><span class="n">D</span> <span class="o">=</span> <span class="n">j</span><span class="p">.</span><span class="n">D</span>
            <span class="n">j</span><span class="p">.</span><span class="n">C</span><span class="p">.</span><span class="n">S</span> <span class="o">-=</span> <span class="mi">1</span>
        <span class="n">j</span> <span class="o">=</span> <span class="n">j</span><span class="p">.</span><span class="n">R</span>

<span class="k">def</span> <span class="nf">_purify</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">node</span><span class="p">):</span>
    <span class="s">"""Purify: tag same-color nodes, remove different-color ones"""</span>
    <span class="n">col</span><span class="p">,</span> <span class="n">color</span> <span class="o">=</span> <span class="n">node</span><span class="p">.</span><span class="n">C</span><span class="p">,</span> <span class="n">node</span><span class="p">.</span><span class="n">color</span>
    <span class="n">i</span> <span class="o">=</span> <span class="n">col</span><span class="p">.</span><span class="n">D</span>
    <span class="k">while</span> <span class="n">i</span> <span class="ow">is</span> <span class="ow">not</span> <span class="n">col</span><span class="p">:</span>
        <span class="k">if</span> <span class="n">i</span><span class="p">.</span><span class="n">color</span> <span class="o">==</span> <span class="n">color</span><span class="p">:</span>
            <span class="n">i</span><span class="p">.</span><span class="n">color</span> <span class="o">=</span> <span class="n">_COMMITTED</span>    <span class="c1"># Same color: apply sticker
</span>        <span class="k">else</span><span class="p">:</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">_hide</span><span class="p">(</span><span class="n">i</span><span class="p">)</span>           <span class="c1"># Different color: remove
</span>        <span class="n">i</span> <span class="o">=</span> <span class="n">i</span><span class="p">.</span><span class="n">D</span>

<span class="k">def</span> <span class="nf">_unpurify</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">node</span><span class="p">):</span>
    <span class="s">"""Backtrack: peel off stickers or restore removed rows, in reverse"""</span>
    <span class="n">col</span><span class="p">,</span> <span class="n">color</span> <span class="o">=</span> <span class="n">node</span><span class="p">.</span><span class="n">C</span><span class="p">,</span> <span class="n">node</span><span class="p">.</span><span class="n">color</span>
    <span class="n">i</span> <span class="o">=</span> <span class="n">col</span><span class="p">.</span><span class="n">U</span>                       <span class="c1"># Note: reverse order
</span>    <span class="k">while</span> <span class="n">i</span> <span class="ow">is</span> <span class="ow">not</span> <span class="n">col</span><span class="p">:</span>
        <span class="k">if</span> <span class="n">i</span><span class="p">.</span><span class="n">color</span> <span class="ow">is</span> <span class="n">_COMMITTED</span><span class="p">:</span>
            <span class="n">i</span><span class="p">.</span><span class="n">color</span> <span class="o">=</span> <span class="n">color</span>         <span class="c1"># Peel sticker, restore original color
</span>        <span class="k">else</span><span class="p">:</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">_unhide</span><span class="p">(</span><span class="n">i</span><span class="p">)</span>         <span class="c1"># Restore removed row
</span>        <span class="n">i</span> <span class="o">=</span> <span class="n">i</span><span class="p">.</span><span class="n">U</span>
</code></pre></div></div>

<hr />

<h2 id="full-modeling-of-the-word-search-puzzle">Full Modeling of the Word Search Puzzle</h2>

<p>With this mechanism in place, translating the word search puzzle into algorithm input is straightforward:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Concept</th>
      <th style="text-align: left">Corresponding “column”</th>
      <th style="text-align: left">Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left">Each word</td>
      <td style="text-align: left">Primary column</td>
      <td style="text-align: left">Must be placed exactly once</td>
    </tr>
    <tr>
      <td style="text-align: left">Each cell</td>
      <td style="text-align: left">Color column</td>
      <td style="text-align: left">Can be covered multiple times, but the color (letter) must be consistent</td>
    </tr>
  </tbody>
</table>

<p>Each “placement” (word + position + direction) corresponds to a row, covering:</p>
<ul>
  <li>The primary column for that word (meaning “this word has been placed”)</li>
  <li>The color column for each cell it passes through, with color = the corresponding letter</li>
</ul>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">create_word_search</span><span class="p">(</span><span class="n">rows</span><span class="p">,</span> <span class="n">cols</span><span class="p">,</span> <span class="n">words</span><span class="p">,</span> <span class="n">allow_diagonal</span><span class="o">=</span><span class="bp">True</span><span class="p">):</span>
    <span class="n">W</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">words</span><span class="p">)</span>
    <span class="n">num_primary</span> <span class="o">=</span> <span class="n">W</span>                  <span class="c1"># First W columns are primary (words)
</span>    <span class="n">num_items</span>   <span class="o">=</span> <span class="n">W</span> <span class="o">+</span> <span class="n">rows</span> <span class="o">*</span> <span class="n">cols</span>    <span class="c1"># Remaining: one column per cell (color)
</span>
    <span class="n">options</span><span class="p">,</span> <span class="n">labels</span> <span class="o">=</span> <span class="p">[],</span> <span class="p">[]</span>

    <span class="k">for</span> <span class="n">w_idx</span><span class="p">,</span> <span class="n">word</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">words</span><span class="p">):</span>
        <span class="k">for</span> <span class="n">dir_name</span><span class="p">,</span> <span class="p">(</span><span class="n">dr</span><span class="p">,</span> <span class="n">dc</span><span class="p">)</span> <span class="ow">in</span> <span class="n">DIRECTIONS</span><span class="p">.</span><span class="n">items</span><span class="p">():</span>
            <span class="k">for</span> <span class="n">sr</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">rows</span><span class="p">):</span>
                <span class="k">for</span> <span class="n">sc</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">cols</span><span class="p">):</span>
                    <span class="c1"># Bounds check
</span>                    <span class="n">er</span> <span class="o">=</span> <span class="n">sr</span> <span class="o">+</span> <span class="n">dr</span> <span class="o">*</span> <span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">word</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span>
                    <span class="n">ec</span> <span class="o">=</span> <span class="n">sc</span> <span class="o">+</span> <span class="n">dc</span> <span class="o">*</span> <span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">word</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span>
                    <span class="k">if</span> <span class="ow">not</span> <span class="p">(</span><span class="mi">0</span> <span class="o">&lt;=</span> <span class="n">er</span> <span class="o">&lt;</span> <span class="n">rows</span> <span class="ow">and</span> <span class="mi">0</span> <span class="o">&lt;=</span> <span class="n">ec</span> <span class="o">&lt;</span> <span class="n">cols</span><span class="p">):</span>
                        <span class="k">continue</span>
                    <span class="c1"># Build this row: primary item + color item per letter
</span>                    <span class="n">option</span> <span class="o">=</span> <span class="p">[(</span><span class="n">w_idx</span><span class="p">,</span> <span class="bp">None</span><span class="p">)]</span>
                    <span class="k">for</span> <span class="n">k</span><span class="p">,</span> <span class="n">ch</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">word</span><span class="p">):</span>
                        <span class="n">r</span><span class="p">,</span> <span class="n">c</span> <span class="o">=</span> <span class="n">sr</span> <span class="o">+</span> <span class="n">dr</span><span class="o">*</span><span class="n">k</span><span class="p">,</span> <span class="n">sc</span> <span class="o">+</span> <span class="n">dc</span><span class="o">*</span><span class="n">k</span>
                        <span class="n">option</span><span class="p">.</span><span class="n">append</span><span class="p">((</span><span class="n">W</span> <span class="o">+</span> <span class="n">r</span><span class="o">*</span><span class="n">cols</span> <span class="o">+</span> <span class="n">c</span><span class="p">,</span> <span class="n">ch</span><span class="p">))</span>
                    <span class="n">options</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">option</span><span class="p">)</span>
                    <span class="n">labels</span><span class="p">.</span><span class="n">append</span><span class="p">((</span><span class="n">w_idx</span><span class="p">,</span> <span class="n">sr</span><span class="p">,</span> <span class="n">sc</span><span class="p">,</span> <span class="n">dir_name</span><span class="p">))</span>

    <span class="n">dlx</span> <span class="o">=</span> <span class="n">DLX_C</span><span class="p">(</span><span class="n">num_primary</span><span class="p">,</span> <span class="n">num_items</span><span class="p">,</span> <span class="n">options</span><span class="p">)</span>
    <span class="n">solutions</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">sol</span> <span class="ow">in</span> <span class="n">dlx</span><span class="p">.</span><span class="n">solve</span><span class="p">():</span>
        <span class="n">placement</span> <span class="o">=</span> <span class="p">[(</span><span class="n">words</span><span class="p">[</span><span class="n">labels</span><span class="p">[</span><span class="n">i</span><span class="p">][</span><span class="mi">0</span><span class="p">]],</span> <span class="o">*</span><span class="n">labels</span><span class="p">[</span><span class="n">i</span><span class="p">][</span><span class="mi">1</span><span class="p">:])</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="n">sol</span><span class="p">]</span>
        <span class="n">solutions</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">placement</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">solutions</span>
</code></pre></div></div>

<p>Let’s run it:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">words</span> <span class="o">=</span> <span class="p">[</span><span class="s">"ABEL"</span><span class="p">,</span> <span class="s">"CANTOR"</span><span class="p">,</span> <span class="s">"GAUSS"</span><span class="p">,</span> <span class="s">"EULER"</span><span class="p">]</span>
<span class="n">solutions</span> <span class="o">=</span> <span class="n">create_word_search</span><span class="p">(</span><span class="mi">6</span><span class="p">,</span> <span class="mi">6</span><span class="p">,</span> <span class="n">words</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">solutions</span><span class="p">))</span>   <span class="c1"># → 244792
</span></code></pre></div></div>

<p>The first solution (all four words neatly in separate rows):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>CANTOR
GAUSS.
EULER.
ABEL..
......
......
</code></pre></div></div>

<p>More interesting solutions involve diagonal placements and letter crossings, all handled automatically by the algorithm — the code needs zero special-casing for these situations.</p>

<hr />

<h2 id="word-rectangles-solving-another-puzzle-along-the-way">Word Rectangles: Solving Another Puzzle Along the Way</h2>

<p>Same framework, different constraints, different puzzle — <strong>word rectangles</strong>: horizontal words fill every row, vertical words fill every column, and intersecting cells must share the same letter.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>C A T
D O G
E E L
</code></pre></div></div>

<p>Across: CAT, DOG, EEL; Down: CDE, AOE, TGL.</p>

<p>The modeling is entirely analogous: rows and columns are each a primary item, cells are color items. The key difference is that primary items change from “each word” to “each row and each column” — each row and column gets a primary item indicating it must be filled with exactly one valid word. Change a few lines of code and you’re done — the underlying algorithm doesn’t need to change at all.</p>

<hr />

<h2 id="stepping-back">Stepping Back</h2>

<p>Let’s review what we did:</p>

<ol>
  <li>Described “word search puzzle” as “each word must be placed exactly once, each cell’s letter must be consistent”</li>
  <li>Translated “letter consistency” into “same color allows multiple coverings”</li>
  <li>Discovered the naive implementation has a double-operation bug</li>
  <li>Fixed it with a sticker (the COMMITTED tag)</li>
</ol>

<p>The most interesting part of the whole process: <strong>we never wrote any “how to solve the puzzle” logic</strong>. We only described “what makes a valid puzzle,” and the algorithm found all the answers on its own.</p>

<p>This is the most compelling quality of this class of search algorithms: describe the constraints clearly, and solving happens automatically. Creating puzzles and solving puzzles use the exact same framework.</p>

<hr />

<h2 id="want-to-try-it-yourself">Want to Try It Yourself?</h2>

<p>Full code is at <a href="https://github.com/morefreeze/morefreeze.github.io/blob/master/code/dlx_colors.py">dlx_colors.py</a> and <a href="https://github.com/morefreeze/morefreeze.github.io/blob/master/code/word_search.py">word_search.py</a> — just clone and run.</p>

<p>A few things to explore:</p>
<ul>
  <li><strong>Try different words</strong>: Plug in your own name or favorite words and see how many ways they can be hidden</li>
  <li><strong>Change the grid size</strong>: Same words, but how different are the solution counts for 6x6 vs 8x8?</li>
  <li><strong>Add a constraint</strong>: What if every word must intersect with at least one other word? How would you modify the model?</li>
</ul>

<p>Feel free to share your findings in the comments.</p>]]></content><author><name>More Freeze</name></author><category term="algorithm" /><category term="knuth" /><category term="exact-cover" /><category term="dancing-links" /><category term="word-search" /><category term="TAOCP" /><summary type="html"><![CDATA[Starting from the newspaper word search game, let's talk about a subtle bug lurking in the algorithm — and how Knuth fixed it with a sticker]]></summary></entry></feed>