JavaScript递归数独求解器回溯失效及p5.js可视化问题排查
回溯卡死故障排查
代码里有两处核心逻辑错误,直接导致校验失效、回溯错位,最终程序卡死:
- 单元格坐标访问完全错误
findEmptyCell()返回值是[行号, 列号]格式的数组,索引0对应行坐标、索引1对应列坐标,但全代码访问单元格时都用了未定义的row、column变量,写的cell[row]、cell[column]实际取到的都是undefined,读写二维数组时坐标完全错位,回溯阶段根本无法正确定位要重置的单元格。
修复方式:拿到空单元格后直接解构坐标,把所有cell[row]/cell[column]替换成明确的行、列变量,示例:
const cell = this.findEmptyCell(); if (!cell) return true; const [r, c] = cell; // 解构拿到行r、列c,后续统一用r、c访问坐标
- 3x3宫格校验逻辑完全失效
现有宫格循环的边界写法错误:
// 原错误代码 var boxRow = Math.floor(cell[row] / 3) * 3; var boxColumn = Math.floor(cell[column] / 3) * 3; for (boxRow; boxRow < boxRow * 3; boxRow++) { for (boxColumn; boxColumn < boxColumn * 3; boxColumn++) { // 校验 } }
这段代码的问题有三个:
- 当宫格起始行/列为0时,循环判断条件为
0 < 0,直接跳过整个第一大行/大列的宫格校验 - 当宫格起始行/列为3时,判断条件为
3 < 9,会遍历6行/列,跨了两个宫格的范围,校验结果完全错误 - 循环直接修改了宫格起始坐标变量,内层循环跑完后列起始值被篡改,后续循环范围完全混乱
正确的宫格校验逻辑如下:
const boxStartR = Math.floor(r / 3) * 3; const boxStartC = Math.floor(c / 3) * 3; for (let i = 0; i < 3; i++) { for (let j = 0; j < 3; j++) { const curR = boxStartR + i; const curC = boxStartC + j; if (this.data[curR][curC] == digit && !(curR === r && curC === c)) { return false; } } }
这两个bug叠加后,isValid会把大量非法填入值判定为合法,程序会沿着错误分支无限递归,既找不到正确解,也无法正确回溯回退,最终触发栈溢出或死循环冻结,和你控制台输出的现象完全吻合——你日志里第一行第三列错误填入1,就是校验失效导致的。
p5.js画布不更新原因及可视化方案
故障原因
浏览器UI渲染和JS执行共用同一个主线程,你现在写的solve()是同步递归函数,启动后会一直占满主线程直到运行结束,浏览器根本没有空闲执行画布重绘,哪怕你在递归里调用drawBoard(),也只是执行了绘图指令,不会把结果刷新到页面上,所以只能看到控制台日志,画布没有任何变化。
实现方案
必须把同步递归求解改成异步步进式求解,把每一步填数、回溯的操作拆成独立小任务,每执行一步就给浏览器留重绘画布的时间:
- 废弃原来的自递归
solve写法,维护一个求解栈,栈里存储当前待处理单元格的坐标、已经尝试过的数字列表 - 借助p5.js自带的
draw()循环(或requestAnimationFrame),每次循环只执行一步求解操作:要么给当前单元格尝试下一个合法数字,要么当前单元格所有数字试完不合法就弹栈回溯,把当前格重置为占位符 - 每帧执行完求解步骤后调用
drawBoard()重绘棋盘,画面就能正常刷新 - 可以通过控制多少帧执行一次求解步骤,自由调整可视化动画的速度
核心改造示例代码:
let board; let solveStack = []; let isSolving = false; const STEP_INTERVAL = 8; // 每8帧走一步,数值越大速度越慢 function setup() { createCanvas(450, 450); board = new Board(); // 初始化求解栈,压入第一个空单元格 const firstEmpty = board.findEmptyCell(); solveStack.push({r: firstEmpty[0], c: firstEmpty[1], tried: []}); isSolving = true; } function draw() { background(255); // 按间隔执行求解步骤 if (isSolving && frameCount % STEP_INTERVAL === 0) { solveOneStep(); } drawBoard(); // 每帧重绘画板 } function solveOneStep() { if (solveStack.length === 0) { isSolving = false; // 无解 return; } const curCell = solveStack.at(-1); let placed = false; // 尝试下一个没试过的数字 for (let digit = 1; digit <= 9; digit++) { if (curCell.tried.includes(digit)) continue; curCell.tried.push(digit); if (board.isValid([curCell.r, curCell.c], digit)) { board.data[curCell.r][curCell.c] = digit; const nextEmpty = board.findEmptyCell(); if (!nextEmpty) { isSolving = false; // 求解完成 return; } // 下一个空单元格入栈 solveStack.push({r: nextEmpty[0], c: nextEmpty[1], tried: []}); placed = true; break; } } // 没找到合法数字,回溯 if (!placed) { board.data[curCell.r][curCell.c] = x; solveStack.pop(); } }
注意:必须先修复前面提到的坐标访问、宫格校验bug,再做可视化改造,否则逻辑错误会导致求解结果不对。
内容的提问来源于stack exchange,提问作者s123
相关产品推荐
相关产品推荐

