「无限题库」生成器实现笔记:唯一解数独、完美迷宫与 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。原因有三:

  1. 这类工具的生命周期在"一次访问"内,框架带来的工程收益体现不出来;
  2. 单文件可以直接部署到任何静态服务器,也被搜索引擎完整抓取(整站 1100+ 页全靠 sitemap 驱动收录);
  3. 文件即文档——后来维护的人打开文件就知道全部逻辑。

唯一的"共享代码"是 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)

💡 关于火力盒子:ahotbox.com 收录 1000+ 款免费纯前端在线工具(开发、文本、时间、生成器、安全等),所有数据在浏览器本地处理,无需注册、不上传数据、支持离线使用。