接下来我会在后面的帖子里说明主要的过程。
2021年12月我编写过一个日历拼图的程序,当时也在网站上写了帖子。这次想解数独时,我想使用那里面的方法,那个的内容比较复杂,程序代码也复杂一些,涉及到日历的各种旋转和翻面,不过,过程比较短,才8个广场。
数独的过程会长一些,但处理会简单一些(只有1-9的数字)。
考虑到数独的特点,使用了一个不算高效但是有效的方式,使用9*9*9个指针,用合适的方式串起来,并且能够回溯。(也许有人会问不是用指针的二维数组就可以了吗?怎么要用三维?这就是我刚才说的,有效但不算高效的意思了。用三维指针数组是一个有效的方式,用二维应该也能实现,但我没采用,因为三维会比较直观,稍后就明白了。)
可能有初学者会问(高手请略过),为什么要用指针,用数组来穷举不可以吗?数组穷举是否可以,我不确定,但我以前在用数组来穷举时,就会遇到无法回溯的问题。但指针就可以回溯,从而找到合适的方式。初学者需要先补充一些关于指针、数据结构方面的基本操作。比如,建立数据结构、定义指针,指针空间的申请,指针的各种关联。其中,数据结构可以认为是C语言甚至是所有语言的高级运用的基础工具,是需要掌握的。
以下是我在解数独问题时关于建立指针体系的具体做法。
先建立头指针head,再建立tmp指针(移动指针、或者称为临时指针)。我先判断是否为空值(在程序里以0表示),如果是0就建立指针。对于第一个空值,就将它设为head的下一指针,也就是head->next=第一个空值的指针,同时第一个空值指针的pre指针也就是head。比如,在本例中,(1,1)的值是空的,所以,第一个指针就是(1,1)坐标的。具体内容是head->next=p[0][0][0](第三维的作用稍后解释)还要设置p[0][0][0]->pre=head,是否有这个必要,其实也是有的。这是用来判断程序无解的重要部分。
然后,再找下一个空值。在本例中,(1,2)是空值,所以,p[0][0][0]->next=p[0][1][0],还有p[0][1][0]->pre=p[0][0][0],以此类推。如下图所示。
最后一个空值的next指针指向NULL,这是判断数独是否解题结束的主要标志。最后一个空值不是哪个指针的pre。
注:那些已经有数的空格,只是摆设,并不会出现在这个指针体系里面,也不会为他们申请结构空间。
这是主线。
接着说明第三维的作用。第三维,其实是用来表示1-9个数值的。
这9个数值是用left和right串起来的。就是1的left是空指针,而right是2,2的left是1,2的right是3。具体的是p[0][0][0]->left-NULL, p[0][0][0]->right=p[0][0][1], p[0][0][1]->left=p[0][0][0],p[0][0][1]->right=p[0][0][2]。
要注意,目前只有p[i][j][0]有next和pre,其余的不设next和pre(默认为NULL或随机值)。
所以,在主线里,只有p[i][j][0]。这是为了方便搜索时是从1开始到9,如下图所示:
指针体系建立之后,开始进行数独的解的搜索。
先是从第一个空值(比如本例的p[0][0][0])开始。从1-9进行搜索,有合适的数值时,就修改head的next数值为这个。比如这次1是可以的。所以,不需要修改什么内容,直接就可以到下一个空值了。
本例中下一个空值是p[0][1][0],从1开始搜索,明显1是不可以了(和前面的1重复了),所以,要数到2。这时候,需要做一些处理。就是上一个指针的next要指向第2个值,本例中是p[0][0][0]->next=p[0][1][1](之前是p[0][1][0]),而相应的p[0][1][1]->pre= p[0][0][0]。因为2是满足要求的。所以,就到第3个空值了。
第3个空值是(1,3),从p[0][2][0]表示的1开始寻找,明显1、2都是不满足要求的,要算到3,这时候,next和pre也要相应处理。
……
如果到了一个空值,比如(i,j),从1-9都尝试过了(right指针指向NULL了),都无法得到满足要求,这时候,就要回溯到上一个空值,要让上一个空值做出变化,但这时候,指针体系要将上一指针体系的next从现在的9调回到1(这步很关键,不然会导致下一次搜索时,不是从1开始,从而不能把1-9全部搜索一点)。
如果上一层也已经搜索到了9,那就要继续向上一层回溯。注意,此时一样要把指针体系的next定位到第1个数值,不然没办法从1开始搜索,可能会导致出错(我在多次回溯时吃过亏,因为无法解决一些数独,从而导致花费多了一天时间进行查找原因)。
按上述的方法执行时,最终就可以出来正确的解答了。