数独求解(javascript实现)

5

看《算法的乐趣》,试着用非递归穷举来解数独,看效率如何!

数独规则

数独游戏,经典的为9×9=81个单元格组成的九宫格,同时也形成了3×3=9个小九宫格,要求在81个小单元格中填入数字1~9,并且数字在每行每列及每个小九宫格中都不能重复。

数独技巧

  • 直观法

  • 候选数法

  • 相关二十格:一个数字只与其所在行列及小九宫格的二十格相关

我的思路

  • 精心设计了有效性判定函数,最多一次遍历81个小单元格就能做出方案的有效性判定。

  • 同理设计了相关20格判定,一次0~9的循环就完成有效性判定。

  • 用数组模拟堆栈,为搜索提供回溯信息。

  • 利用对象具有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 ] ]

如果觉得我的文章对你有用,请随意赞赏

你可能感兴趣的

10 条评论
善良的杰夫 · 2016年04月25日

为什么每次都判断整个数独是否有效呢?可以填入一个数字之后,只判断这个数字在数独中是否有效,效率更高。checkValid(sudo, i, j)

回复

zhoutk 作者 · 2016年04月25日

有空我试试,偷了个懒被你发现了,呵呵!

回复

zhoutk 作者 · 2016年04月25日

谢谢 @善良的杰夫 提醒,加入相关二十格判定,效率提高了五倍以上。

回复

善良的杰夫 · 2016年04月25日

只与它周围20格有关是什么意思?

回复

zhoutk 作者 · 2016年04月25日

根据数独规则,同行同列加上所在九宫格,一共是二十一个小方格,除它自己,其余二十个是与它相关的。也就是一个小方格的变动只会影响同行同列同小九宫格的二十个方格,不与其它方格相关。

回复

stranger · 2017年11月22日

000000000000000250003409000080006000000052009007000000200000000060700000000300074

你这个算法是真穷举啊 碰到上例中的题目就需要很久了.... 能不能再优化一下呢?

一共尝试了56566999次,50秒

回复

stranger · 2017年11月22日

090400007500060000000000000000000860040000000030907000602000000000503000800000050
169493384次尝试 159秒

回复

stranger · 2017年11月22日

800000060000000000300405000096080000002000000000001004030060090400000001007000000
228562033次尝试 202秒

回复

stranger · 2017年11月22日

大兄弟 看到消息了优化下啊 等你的消息

回复

doll · 2018年05月24日

码云 访问受限..

回复

载入中...