JavaScript非递归穷举法解数独实现与优化

作者:缓存笔记

时间:2026-10-30

浏览:662

本文介绍使用JavaScript实现数独求解的非递归穷举方案。通过二维数组存储盘面,利用对象哈希特性优化行列及小九宫格的冲突判定,结合一维数组模拟堆栈实现回溯。该方案无需开辟额外存储空间,针对特定难度题目可在1秒内完成求解,展示了高效的算法设计与实现细节。

文章目录

解数独

参考《算法的乐趣》一书,尝试采用非递归穷举策略求解数独,并评估其运行效率。

数独规则

标准数独游戏由9×9共81个单元格组成的九宫格构成,内部包含9个3×3的小九宫格。规则要求在所有81个单元格中填入数字1至9,确保每一行、每一列以及每一个小九宫格内的数字均不重复。

数独技巧

  • 直观法
  • 候选数法
  • 相关二十格:指一个数字仅与其所在的行、列及小九宫格内的其他二十个格子存在关联。

我的思路

  • 精心设计了有效性判定函数,仅需单次遍历81个小单元格即可完成方案有效性的判定。
  • 同样设计了相关20格判定逻辑,通过一次0至9的循环即可高效完成有效性检查。
  • 使用数组模拟堆栈结构,为搜索过程提供必要的回溯信息支持。
  • 利用JavaScript对象具备的Map(映射)性质,辅助判断方案有效性,从而大幅简化算法逻辑。

方案设计与实现

整体实现仅依赖一个二维数组存储数独方案,一个一维数组作为堆栈,以及一个布尔变量作为回溯标识。

变量定义

var problem = [                //这是书上提到的难度10.7的题
    [8,0,0,0,0,0,0,0,0],
    [0,0,3,6,0,0,0,0,0],
    [0,7,0,0,9,0,2,0,0],
    [0,5,0,0,0,7,0,0,0],
    [0,0,0,0,4,5,7,0,0],
    [0,0,0,1,0,0,0,3,0],
    [0,0,1,0,0,0,0,6,8],
    [0,0,8,5,0,0,0,1,0],
    [0,9,0,0,0,0,4,0,0]
]
var stack = [],flag = false;

方案有效性判定

充分利用JavaScript对象的哈希特性。为便于调试,判定有效时函数返回0;无效时分为三种情况:行冲突、列冲突、小九宫格冲突,分别返回1、2、3。该函数主要用于前期判定,后续引入相关二十格判定后,在寻找答案阶段此函数不再使用。

function checkValid(sudo){
    let subSudo = {}                        //辅助变量,用来判定小九宫格是否冲突
    for(let i = 0; i<9; i++){
        let row = {}, col = {}              //辅助变量,用来判定行、列是否冲突
        for(let j = 0; j<9; j++){
            let cur1 = sudo[i][j], cur2 = sudo[j][i]            //一次内循环同时完成行列的判定
            if(row[cur1])                    //当前元素已经在行中出现,优化掉零的判断,key为0时值为0,不需要额外判断
                return 1;                    //返回错误代码
            else
                row[cur1] = cur1            //当前元素未在行中出现,存入辅助变量中   
            if(col[cur2])                    //列的判定与行类似,优化掉零的判断,key为0时值为0,不需要额外判断
                return 2;
            else
                col[cur2] = cur2;
            let key = Math.floor(i/3)+'-'+Math.floor(j/3)        //为不同的小九宫格生成不同的key
            if(subSudo[key]){                 //小九宫格中已经有元素,优化掉零的判断,key为0时值为0,不需要额外判断
                if(subSudo[key][cur1])        //对某一个小九宫格的判定与行类似
                    return 3
                else
                    subSudo[key][cur1] = cur1
            }else{                            //这是某小九宫格中的第一个元素
                subSudo[key] = {}             //为小九宫格新建一个辅助变量,并将第一个元素存入其中
                subSudo[key][cur1] = cur1
            }                  
        }
    }
    return 0;                                //程序能运行到这,说明方案有效
}

相关二十格判定

其原理与整体判定相同,核心亮点在于小九宫格位置的精准定位。

function check20Grid(sudo,i,j){                
    let row = {}, col = {}, subSudo = {}                //辅助变量
    for(let k = 0; k < 9; k++){
        let cur1 = sudo[i][k], cur2 = sudo[k][j]
        if(cur1){                                        //当前元素已经在行中出现,优化掉零的判断,key为0时值为0,不需要额外判断
            if(row[cur1])
                return 1;                                //返回错误代码
            else
                row[cur1] = cur1                        //当前元素未在行中出现,存入辅助变量中
        }
        if(cur2){                                        //列的判定与行类似,优化掉零的判断,key为0时值为0,不需要额外判断
            if(col[cur2])
                return 2;
            else
                col[cur2] = cur2;
        }
        //转化循环变量到小九宫格的坐标
        let key = sudo[Math.floor(i/3)*3 + Math.floor(k/3)][Math.floor(j/3)*3+Math.floor(k%3)]
        if(subSudo[key])                                //九宫格判定与行类似,优化掉零的判断,key为0时值为0,不需要额外判断
            return 3
        else
            subSudo[key] = key
    }
    return 0;
}

遍历求解

利用元素状态初值为零即代表待定的特性,并配合堆栈辅助,无需开辟额外的存储空间即可完成求解。

function findAnswer(){
    for(let i = 0; i<9; i++){
        for(let j = 0; j<9; ){
            if(problem[i][j] === 0 || flag){              //当前位置为待定元素的首次处理或回溯到当前位置,两种情况看似不同,其实处理相同,自加1即可
                flag = false;
                let k = problem[i][j] + 1;                //搜索向下一个合法值迈进
                while(k<10){                              //循环找到下一个合法值
                    problem[i][j] = k;                    //填值
                    if(check20Grid(problem,i,j) == 0){    //判定合法,相关二十格判定
                        stack.push([i,j++])               //存储回溯点,并步进
                        break;
                    }
                    k++;
                }
                if(k>9){                                  //当前位置找不到合法值,回溯
                    problem[i][j] = 0;                    //回溯前归零
                    let rt = stack.pop();                 //堆栈中取回溯信息
                    if(!rt)                               //无解判断,返回0
                        return 0;    
                    i=rt[0]                               //穿越
                    j=rt[1]
                    flag = true;
                }
            }else{                                        //当前位置数字为题目给定
                j++;
            }
        }
    }
    return 1;                                            //成功找到一组解
}

完整代码

代码托管于开源中国平台,其中sudoku.js文件即为数独解法的具体实现。

https://git.oschina.net/zhoutk/test.git

答案

针对书中提到的难度为10.7的题目,本方案可在1秒内得出答案,效率表现良好。

[ [ 8, 1, 2, 7, 5, 3, 6, 4, 9 ], [ 9, 4, 3, 6, 8, 2, 1, 7, 5 ], [ 6, 7, 5, 4, 9, 1, 2, 8, 3 ], [ 1, 5, 4, 2, 3, 7, 8, 9, 6 ], [ 3, 6, 9, 8, 4, 5, 7, 2, 1 ], [ 2, 8, 7, 1, 6, 9, 5, 3, 4 ], [ 5, 2, 1, 9, 7, 4, 3, 6, 8 ], [ 4, 3, 8, 5, 2, 6, 9, 1, 7 ], [ 7, 9, 6, 3, 1, 8, 4, 5, 2 ]]

以上为javascript算法解数独实现方案示例的详细内容,更多关于javascript算法解数独的资料请关注本站其它相关文章!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章