解决方法
计算机因其强大的算力和稳固的记忆性,使其相比人类求解数独,天然的具备更优越的条件。也使得一些工作量相对大量,逻辑思维要求较强的方法重新回到选择视线当中。笔者主要运用以 “八皇后问题”为例讲解的回溯思想,结合人类求解数独的两类基本思路,得出通过双重循环实现盘面遍历,借助深度优先的递归思维实现填入数字并依情况进行回溯,从而求解数独的思路。
该方法既要借鉴直接观察填入法中的试错思路,通过不断进行符合游戏规则的试错推进解题进度;又要借鉴人类求解数独中“间接候选”思路以减少每递归层的分支,从而降低时间复杂度和空间复杂度,实现简单的算法优化。
与“八皇后问题”中仅通过行遍历或列遍历实现深度优先算法不同,数独因其需要用1~9间的数字填满盘面而不违反规则,使得对数独的遍历回溯必须要进行全盘遍历。即以双重循环为基础,实现盘面遍历。
回溯思路存在深度优先和广度优先两大类,但从数独的一般解题顺序和其复杂的逻辑特征来看,选择深度优先进行递归尝试是较为常规且稳妥的解题思路。即以深度优先的递归思路进行试错,实现填入数据并依情况进行回溯。
算法优化
若采用常规思维,则需构建一个判断函数(用于判断方格内能否再填入一个数)和一个查询函数(用于查询方格内能填入哪些数),并在深度优先遍历过程中反复调用,从而造成大量时间开销。同时需要对9×9盘面内每一个小方格构建一个长度为9的int数组用于存储展示行列粗块(3×3)中哪些数字已经出现,并依次发现未出现的可填入数字,从而造成大量空间开销。
采用二进制优化后,可以通过一个九位二进制数(右起第一位为0位,至最左共9位0-1数,)的十进制整数,来取代长度为9的int数组,当数字i(1<=i<=9)可以填入空格时,该格的九位二进制数第i-1位为1。相反的,当某个十进制整数的九位二进制数第K位(0<=k<=8)为1时,则暗含数字k+1可以填入。例如:九位二进制数(000101010)表示2,4,6已经在该格行、列、粗块中未出现,同时符合逻辑判别,可以填入该格。
通过一定的实验和思考加之查阅相关资料发现:对任意的数i, {i & (-i)}保留 i 二进制表示中最低位的 1;可以用 i 和 i-1进行按位与运算、用i和最低位的 1 进行按位异或运算来消去i二进制表示中最低位的1;将数i与数 {1 << k} 进行按位异或运算,可以对数i的二进制表示中第k位实现二进制0-1转换等,借助这类二进制的简化查询和使用,可以避免查询函数的构造和大量存储空间的需求。
另一方面,当某格九位二进制数中只有一位为1时,可以直接在该格中填入对应位数字,以达到减少递归深度的优化效果。
算法实现
1.基本数据结构:
长度为9的int数组 line:用于存储每行中出现的数字(以九位二进制表示)
长度为9的int数组column:用于存储每列中出现的数字
3×3大小的int二维数组 block:用于存储盘面内9个粗块中出现的数字
[int,int]类型的动态数组space:用于存储盘面内空白块的位置[i,j]
2.用于在填写盘面和回溯时实现line、column、block更新的函数update:
主要原理:将数i与数 {1 << k} 进行按位异或运算,实现对数i的二进制表示中第k位的二进制0-1转换
3.主调用函数doSudo用于实现数独求解:
1)通过双重循环遍历盘面board,并调用update函数,更新line、column、block值:
2)查找出空白格内候选值唯一的块[i,j],直接向其中填入该值,以减少后续递归层数和分支数。
3)待直接填入完成后,再查找出需要填入的空白格,用space记录,供dfs函数进行深度优先遍历。
4.深度优先遍历函数dfs:
主要原理:对任意的数i, {i & (-i)}保留 i二进制表示中最低位的1。
借助Integer.bitCount(i)自带函数,可以统计i的二进制表示中1的个数。
通过line、column、block三者的或运算获取到该空白块不可取的值。
通过深度优先遍历经典的: “进入->自递归->退出”思路,获得如下算法代码:
5.编写主函数类调用该数独求解类:
该类主要用于构建未求解的数独盘面border[][],并通过创建sudo类的对象,调用doSudo函数实现计算机求解数独,并通过双重循环输出,具体如下图:
实验结果:程序顺利得出符合数独规则结果,耗时6S: