You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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++) {
        // 校验
    }
}

这段代码的问题有三个:

  1. 当宫格起始行/列为0时,循环判断条件为0 < 0,直接跳过整个第一大行/大列的宫格校验
  2. 当宫格起始行/列为3时,判断条件为3 < 9,会遍历6行/列,跨了两个宫格的范围,校验结果完全错误
  3. 循环直接修改了宫格起始坐标变量,内层循环跑完后列起始值被篡改,后续循环范围完全混乱
    正确的宫格校验逻辑如下:
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(),也只是执行了绘图指令,不会把结果刷新到页面上,所以只能看到控制台日志,画布没有任何变化。

实现方案

必须把同步递归求解改成异步步进式求解,把每一步填数、回溯的操作拆成独立小任务,每执行一步就给浏览器留重绘画布的时间:

  1. 废弃原来的自递归solve写法,维护一个求解栈,栈里存储当前待处理单元格的坐标、已经尝试过的数字列表
  2. 借助p5.js自带的draw()循环(或requestAnimationFrame),每次循环只执行一步求解操作:要么给当前单元格尝试下一个合法数字,要么当前单元格所有数字试完不合法就弹栈回溯,把当前格重置为占位符
  3. 每帧执行完求解步骤后调用drawBoard()重绘棋盘,画面就能正常刷新
  4. 可以通过控制多少帧执行一次求解步骤,自由调整可视化动画的速度
    核心改造示例代码:
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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 12:39:15