数独生成器:挖洞、唯一解,以及一个把页面卡死的 bug
生成器先填一个完整解,再逐格挖洞并验证唯一性。我在求解器里把「不限」写成 0,结果空盘要枚举全部解,点新局直接卡死。
数独 的题目不是静态写死的,每次开新局都在浏览器里现生成。做法分两步:先随机填出一个完整解,再逐格挖洞、边挖边验证唯一性。
第一步:填完整解
用一个带 MRV(最小剩余候选)的回溯:每次选候选最少的空格,候选顺序用随机数打乱。空盘上第一格有 9 个候选、第二个 8 个……因为「任何完整赋值都是解」,贪心基本不会撞墙,毫秒级完成。
第二步:挖洞与唯一性
把 81 格随机排序,依次尝试挖掉:移走一个数,然后用数解器数一下解的数量,只要多于 1 就把它放回去。剩下的空格数就是难度:
| 难度 | 目标空格数 |
|---|---|
| 简单 | 42 |
| 中等 | 50 |
| 困难 | 56 |
(17 个已知数造出唯一解是理论上限,实践中没人拿它当难度起点。)
bug:0 不是「不限」
数解器和求解器共用同一个回溯函数,只差一个参数。求解器传 0 表示「找一个解」,数解器传 2 表示「最多数两个」。问题在于 search 把 limit <= 0 解释成了不限:
export function solve(grid, rng) {
if (conflictSet(grid).length) return null;
return search(grid, rng, 1).board;
}
// limit 是「数到几个就停」:0 不是「不限」,而是「一个就够」
const cap = limit > 0 ? limit : 1;
于是 solve(空盘) 变成了「枚举全部数独解」。这个数字约 6.7×10^21 个 —— 页面点「新局」之后转圈不动,卡在第一步,永远不会到挖洞。
修法是把 limit <= 0 归一到 1。语义上也更顺:求解的语义就是「一个就够」,只有数解才需要上限。
为什么 981 项断言之前没发现
因为没人测过「空盘求解」。测试全都在已知题目上跑,而那里恰好只有一个解,枚举与提前停止的差别只是慢一点,看不出来。补上的那条断言是:generate 出来的题必须能解、解必须完整、且解唯一 —— 它同时钉住了两边。
用一个参数表达两种语义时,「0 / null / 不限」是最容易埋下无限循环的地方。

评论
…