「无限题库」生成器实现笔记:唯一解数独、完美迷宫与 SVG 五线谱
火力盒子技术专栏 · 本文是 8 个在线打印生成器的实现复盘,全部纯前端、零依赖
上一期讲了打印 CSS 的坑,这篇复盘三个更有意思的算法问题:怎么保证生成的数独有且只有一个解、怎么生成必然走得通的迷宫、以及五线谱这种"看起来该用图片"的东西为什么反而该用 SVG。三个问题共用一套工程约束:单文件 HTML、无依赖、秒级出结果、结果可无限刷新。
一、唯一解数独:挖洞时实时验证
数独生成 = 生成终盘 + 挖洞。难点在挖洞:挖多了会出现多解,孩子打到一半发现"解不下去了"。
生成终盘用随机化回溯——每个格子按打乱顺序试数字 1-9,冲突就回溯,一个 DFS 就填满:
function dfs(pos) {
if (pos === n * n) return true;
const r = (pos / n) | 0, c = pos % n;
for (const v of shuffled(candidates)) {
if (fits(r, c, v)) {
grid[pos] = v;
if (dfs(pos + 1)) return true;
grid[pos] = 0;
}
}
return false;
}
唯一解验证用一个限定计数的求解器:数解的数量,数到 2 立即剪枝返回。位运算存行/列/宫的占用掩码,rows[r] | cols[c] | boxes[b] 三步取并集判断候选:
function countSolutions(grid, limit) {
// ...回溯求解,count >= limit 即整体返回
}
挖洞主循环就变得很直白:
for (const pos of shuffled(allPositions)) {
if (removed >= target) break;
const keep = puzzle[pos];
puzzle[pos] = 0;
if (countSolutions(puzzle.slice(), 2) === 1) removed++;
else puzzle[pos] = keep; // 多解,放回
}
性能实测:9×9 挖 46 洞(困难档)全程 5ms 以内——位掩码求解器比数组扫描快一个数量级,而且"数到 2 就停"剪掉了绝大部分搜索树。
泛化:把宫尺寸做成参数 bh × bw,同一套代码直接支持 4×4(2×2 宫)、6×6(2×3 宫)、9×9(3×3 宫),儿童入门和标准模式零成本共存。
二、完美迷宫:DFS 回溯 + 全连通性质
迷宫生成用 recursive backtracker。用位掩码存每格四面墙(上1右2下4左8):
const stack = [[0, 0]];
visited[0] = true;
while (stack.length) {
const [r, c] = stack[stack.length - 1];
const neighbors = unvisitedNeighbors(r, c);
if (!neighbors.length) { stack.pop(); continue; }
const d = pick(neighbors);
walls[r * n + c] &= ~d.wall; // 打通当前格的墙
walls[nr * n + nc] &= ~d.opp; // 打通邻居的对面墙
visited[nr * n + nc] = true;
stack.push([nr, nc]);
}
这个算法天然产出完美迷宫(perfect maze):任意两格之间恰有一条路径。也就是说:
- 不存在孤岛区域(不会生成"走到一半发现出口根本不通"的废图);
- 起点到终点路径唯一(但岔路无数,试错空间足够大)。
这两条性质是"数学保证"而不是"抽查验证",所以每一张图都敢承诺可解。
答案页用 BFS 求最短路——墙掩码现成,无权图 BFS 就是 O(V+E),起点 0 到终点 n²-1,回溯 prev 数组得到路径,SVG polyline 连线画成红色答案。
渲染用 SVG line 逐格画墙:只画每格的上墙和左墙避免重复描线,最后一行/列补下墙/右墙收边。shape-rendering: crispEdges 让线条在打印缩放下保持锐利。
三、五线谱:为什么该用 SVG 而不是图片
五线谱的直觉做法是贴一张空白谱纸图片。但图片有三个致命伤:分辨率固定(打印发虚)、间距不可调、行数多了会错位。
SVG 方案里一行谱表就是若干条 <line>:五线间距是参数、每行小节数决定竖线位置、行尾终止线用 2px 粗线(乐谱排版惯例)。大谱表(钢琴)就是两组五线共享小节竖线。整个"纸"是程序绘制的,换任何行数/小节数组合都精确对齐,打印矢量无损。
一个细节:谱表要塞满一页时,行数 × 行高可能超过 A4 可用高度。做法是计算 scale = 可用高度 / 实际总高,超了就整体压缩线间距——保证任何配置都恰好一页,而不是溢出到第二页。
工程结构上的一个选择:生成器 vs 数据
8 个生成器(口算、字帖、控笔、数独、迷宫、单词默写、单词搜索、五线谱)没有用框架,全部是单文件 HTML + 原生 JS。原因有三:
- 这类工具的生命周期在"一次访问"内,框架带来的工程收益体现不出来;
- 单文件可以直接部署到任何静态服务器,也被搜索引擎完整抓取(整站 1100+ 页全靠 sitemap 驱动收录);
- 文件即文档——后来维护的人打开文件就知道全部逻辑。
唯一的"共享代码"是 CSS 基座(tool-base.css)和注入导航页脚的 tool-chrome.js,页面本体保持独立。
效果
这 8 个生成器上线后,同一个设置每次生成的题目、迷宫、谜题都不同——等于无限题库。9×9 困难数独从点击到出题 < 100ms,20×20 迷宫 < 50ms,全部在手机浏览器里流畅运行。
在线体验(含全部源码可右键查看):https://www.ahotbox.com/tools/education/print-studio.html
(发布到 CSDN 时请删除上一行的外链,或替换为纯文本域名 ahotbox.com)