1. Danclink links 算法解Sudoku的基本思想:
将Sudoku问题转化为等价的01矩阵问题,然后用dlx算法求解。从一个由0 1组成的矩阵中找到一个行的集合,使得集合中每列恰好包含一个1,称为01矩阵问题。解决它的dancling links 算法利用双向链表的一个性质,巧妙的剪枝,获得了较好的性能
A) 构造01矩阵
01矩阵问题是什么?
01矩阵是这样一个问题::给定一个由0和1组成的矩阵,是否能找到一个行的集合,使得集合中每一列都恰好包含一个1?
例如,下面这个矩阵
| (3) |
包含了这样一个集合(第1,4,5行)。
第1行:0 0 1 0 1 1 0
第4行:1 0 0 1 0 0 0
第5行:0 1 0 0 0 0 1
每1列恰1个1,符合条件。
如何用Sudoku构造01矩阵
Sudoku问题可以转化为01矩阵问题。可以知道,Sudoku有如下四个约束条件
1)每一个数(1到9)在每一行出现且仅出现一次。
2)每一个数(1到9)在每一列出现且仅出现一次。
3)每一个数(1到9)在每一个九宫格出现且仅出现一次。
4)每一个格子出现且仅出现一个数。
注意:第四个条件很容易漏掉,但这是必不可少的。
下面是Sudoku转化为01矩阵的方法
1)令r表示行,c表示列,b表示九宫格,k表示值,i表示一个格子,那么rk,ck,bk, i
即可表示4个约束条件,(例如rk=15,表示第一行只有1个5,bk=32,表示第3个九宫格只有1个2,i=32表示第32个格子只有一个数。因为r, c , b, k 范围为1到9,i范围为1到81,所以总共有9*9*3+81=324种情形,即表示01矩阵中有324列)
2)考虑每一个格子的情况,如果第2行第4列是5(rck=245)那么可知rk=25,ck=45, bk=25, i=13这四列下的1找到了。(为什么bk=25,因为按从左到右,从上到下的顺序,第2行第四列恰为第2个九宫格,同理i=13也是这个道理),那么可以生成这样一行01序列
0 0 0 0……1…0 0 0…1…0 0 0…1…0 0 0 0 0 0 0 0…1……0 0 0 0
这一行总共324个数,表示324行,其中4个1分别位于rk=25,ck=45, bk=25, i=13这四列。
假如第4行第2列是未知的。那么可知第4行第2列可能取值为1到9,共九种可能。(即rck=421, 422,423, 424,……429)这9种可能可以生成01矩阵中的9行。例如rck=422可以生成这样一行0 0 0 0 ……1…0 0 0…1…0 0 0…1…0 0 0 0 0 0 0 0…1……0 0 0 0。其中,四个一位于rk=42,ck=22, bk=42, i=29这四列下。
根据以上可构造出1个01矩阵,可知每一列下只可恰好包含一个1。如果我们选出一些行(确切来说是81行)使符合这个条件,那么就可以得到Sudoku的结果。那么danclink link X 算法是如何求解的呢?
B)dancing link X 解01矩阵
考虑下列的求解过程:
如果01矩阵A是空的,问题解决;成功终止。
否则,选择一个列c(确定的)。
选择一个行r,满足 A[r, c]=1 (不确定的。注意这里的r c是矩阵A的行与列,与上面不同)。
把r包含进部分解。(假设这第r行是所求行集合的一部分)
对于所有满足 A[r,j]=1的j,
从矩阵A中删除第j列;
对于所有满足 A[i,j]=1的i,
从矩阵A中删除第i行。
在不断减少的矩阵A上递归地重复上述算法。
如果我们删到最后得到A是空的。那么我们就得到了01矩阵问题的一个解。问题是当我们得不到A是空的时候,我们必须回溯,我们必须把已经错误删掉的一些行与列恢复。这就会用到双向链表的一个有用性质。
以下详细算法:
一个实现X算法的好方法就是将矩阵A中的每个1用一个有5个域L[x]、R[x]、U[x]、D[x]、C[x]的数据对象(data object)x来表示。矩阵的每行都是一个经由域L和R(“左”和“右”)双向连接的环状链表;矩阵的每列是一个经由域U和D(“上”和“下”)双向连接的环状链表。每个列链表还包含一个特殊的数据对象,称作它的表头(list header)。
这些表头是一个称作列对象(column object)的大型对象的一部分。每个列对象y包含一个普通数据对象的5个域L[y]、R[y]、U[y]、D[y]和C[y],外加两个域S[y](大小)和N[y](名字);这里“大小”是一个列中1的个数,而“名字”则是用来标识输出答案的符号。每个数据对象的C域指向相应列头的列对象。
表头的L和R连接着所有需要被覆盖的列。这个环状链表也包含一个特殊的列对象称作“根”,h,它相当于所有活动表头的主人。而且它不需要U[h]、D[h]、C[h]、S[h]和N[h]这几个域。
举个例子,(3)中的0-1矩阵将用这些数据对象来表示,就像图2展示的那样,我们给这些列命名为A、B、C、D、E、F和G(这个图表在上下左右处“环绕扭曲”。C的连线没有画出,因为他们会把图形弄乱;每个C域指向每列最顶端的元素)。
|
图2 完全覆盖问题(3)的四方向连接表示法 |
我们寻找所有精确覆盖的不确定性算法现在可以定型为下面这个明析、确定的形式,即一个递归过程search(k),它一开始被调用时k=0:
如果 R[h]=h,打印当前的解(见下)并且返回。
否则选择一个列对象c(见下)。
覆盖列c(见下)。
对于每个r←D[c],D[D[c]],……,当 r!=c,
设置 Ok<-r;
对于每个j←R[r],R[R[r]],……,当 j!=r,
覆盖列j(见下);
search(k+1);
设置 r←Ok且 c←C[r];
对于每个j←L[r],L[L[r]],……,当 j!=r,
取消列j的覆盖(见下)。
取消列c的覆盖(见下)并且返回。
输出当前解的操作很简单:我们连续输出包含O0、O1、……、Ok-1的行,这里包含数据对象O的行可以通过输出N[C[O]]、N[C[R[O]]]、N[C[R[R[O]]]]……来输出。
为了选择一个列对象c,我们可以简单地设置c<-R[h];这是最左边没有覆盖的列。或者如果我们希望使分支因数达到最小,我们可以设置s<-无穷大,那么接下来:
对于每个j←R[h],R[R[h]],……,当 j!=h,
如果 S[j]<s 设置 c←j且 s←S[h]。
那么c就是包含1的序数最小的列(如果不用这种方法减少分支的话,S域就没什么用了)。
覆盖列c的操作则更加有趣:把c从表头删除并且从其他列链表中去除c链表的所有行。
设置 L[R[c]]←L[c]且 R[L[c]]←R[c]。
对于每个i←D[c],D[D[c]],……,当 i!=c,
对于每个j←R[i],R[R{i]],……,当 j!=i,
设置 U[D[j]]←U[j],D[U[j]]←D[j],
并且设置 S[C[j]]←S[C[j]]-1。
操作(1),就是我在本文一开始提到的,在这里他被用来除去水平、竖直方向上的数据对象。
最后,我们到达了整个算法的尖端,即还原给定的列c的操作。这里就是链表舞蹈的过程:
对于每个i←U[c],U[U[c]],……,当 j!=i,
对于每个j←L[i],L[L[i]],……,当 j!=i,
设置 S[C[j]]←S[C[j]]+1,
并且设置 U[D[j]]←j,D[U[j]]←j。
设置 L[R[c]]←c且 R[L[c]]←c。
注意到还原操作正好与覆盖操作执行的顺序相反,我们利用操作(2)来取消操作(1)。(其实没必要严格限制“后执行的先取消”,由于j可以以任何顺序穿过第i行;但是从下往上取消对行的移除操作是非常重要的,因为我们是从上往下把这些行移除的。相似的,对于第r行从右往左取消列的移除操作也是十分重要的,因为我们是从左往右覆盖的。)
|
图3 图2中第A列后面的链表被覆盖 |
考虑一下,例如,对图2表示的数据(3)执行search(0)会发生什么。通过从其他列移除A的行来将其覆盖;那么现在整个结构就成了图3的样子。注意现在D列出现了不对称的链接:上面的元素首先被删除,所以它仍然指向初始的邻居,但是另一个被删除的元素指向了列头。
继续search(0),当r指向(A,D,G)这一行的A元素时,我们也覆盖D列和G列。图4展示了我们进入search(1)时的状态,这个数据结构代表削减后的矩阵
| (4) |
现在search(1)将覆盖B列,而且C列将没有“1”。因此search(2)将什么也找不到。接着search(1)会找不到解并返回,图4的状态会恢复。外部的过程,search(0),将把图4变回图3,而且它会让r前进到(A,D)行的A元素处。
|
图4 图3中D列和G列后的链被覆盖 |
很快就能找到解,并输出
A | D |
|
B | G |
|
C | E | F |
如果在选择c的时候无视S域,会输出
A | D |
|
E | F | C |
B | G |
|
以上内容参考了网上很多资料,在此表示谢意