算法笔记(VII) X算法与十字链表

近毕业论文吃紧,自然上线的时间少了许多。本来想在上个星期做一下关于的Donald Knuth的algorithm X with Dancing link的,不过本着实践第一的原则,直到编完了整个程序,我才想写一写关于这个算法,抑或仅仅是一个编程技巧的dancing link。

第一听说dancing link(由于其数据结构过于巧妙,使得数据结构成为了算法的名字)是从做ACM的师弟那里听到得,当时只是感觉这个东西的用途似曾相识,适合的问题将会很多。简单的说,这个算法是要从0-1矩阵中选出若干的行,使得这些被提取出的行构成的子矩阵的每一列均有且只有一个1。相信做过0-1规划和约束满足问题的同学就会意识到这是一个多么普适的模型。一大类的问题均可以转化成0-1整数规划问题,包括exact cover problem(即Knuth基于1979年一篇关于实现回溯算法的技巧论文,而提出Dancing link去求解的原问题)、set cover problem等等。 这些问题都是NP难(最小覆盖)或NP完全问题(k个点判定问题),而dancing link求解这类问题可以达到很高的效率。

简单的来说,Dancing Link 是一个循环十字链表,每一个链表表示0-1矩阵的非零元素,例如我们可以这样定义每一个节点

struct node {

node* left, *right, *up ,*down; // 十字链表的上下左右指针

int row, col; //表示这个节点对应矩阵的行和列号

}

对于上面的结构,其实不难理解。首先十字链表的四个指针域是必须的。其次,下面的两项,对应矩阵的行号和列号。在实际的编程中,这两个域是非常有用的,行号将作为最终的答案返回给我们。而列号将方便我们快速的定位到节点所述的列头节点,如下就是Knuth给出的示意图:


图片

其中节点h是整个dancing link 的入口,而A-G是每个列的头节点,这些节点不对应0-1矩阵的元素,他们的作用是方便我们操作数据结构:dancing link。特别是每一个列的头结点有一个特别的域用于记录当前列的非零节点的数目。相比于上述的节点,以下的节点将一一对应一个矩阵的非零点了。如上图将对应0-1矩阵(6行7列):

  __1 __2 __3__4__5__6__7_

1[   0     0     1    0    1    1    0 ]

2[   1     0     0    1    0    0    1 ] 

3[   0     1     1     0    0    1   0 ]

4[   1     0     0     1    0    0   0 ]

5[   0     1     0     0    0    0   1 ]

 6[  0     0     0     1    1    0   1 ] 

 

以上的0-1矩阵问题参见[wiki].

下面要说一说,dancing link之所以高效的原因了。一是由于其结构的特殊性;二则是其剪枝的条件非常的强,扩展的状态节点数目相对于原空间要少很多;Dancing link 的框架仍然是DFS回溯搜索,但是由于其特殊的数据结构省去了大量的栈空间以及递归调用开销,因此可以达到很高的效率。在wiki上,有关于algorithm X的框架说明:

  1. If the matrix A is empty, the problem is solved; terminate successfully.
  2. Otherwise choose a column c (确定的).
  3. Choose a row r such that Arc = 1 (非确定的).
  4. Include row r in the partial solution.
  5. For each column j such that Arj = 1,
    for each row i such that Aij = 1,
    delete row i from matrix A;
    delete column j from matrix A.
  6. Repeat this algorithm recursively on the reduced matrix A.

可以看出,上述过程仍然是一个回溯搜索思路。简单的来说,我们选择一个非零元素最少的某一列,选择其上非零节点对应的行,将其加入我们的解中去,显然一旦加入了这个节点,为了保证我们的约束能够满足,这个行上的非零点对应的列(即约束条件)即获得了满足,因此可以讲这些列和这些列上的非零元素对应的行统统删除。因为我们的约束条件是:满足性和唯一性。显然这么强的剪枝条件,搜索树的的节点数目大大缩减了。然后,递归的做下去。我们递归的base condition是矩阵的列都没了(矩阵为空,即所有的约束都满足了),我们得到一个解集。反之,我们发现某一列全是零,显然我们已经无法满足这个约束了,我们的求解失败了。按照回溯的思路,则回到上一层。

但是要高效的完成2,3,5这3个耗时的步骤,就一番考虑了。需要dancing link。

对于2步,因为我们有了列头结点,可以方便的得到那个非零元素最少的列。

对于3步,沿着上述的列头节点,我们可以很快的找到一个节点。

这里简单提到,对应于DFA和NFA,我们的2和3步分别是确定的,和非确定的。这一点也就是为什么knuth将algorithm X的原因,因为他认为,对于第3步,我们可以有不同的策略进行选取,如A*算法等等,这就对应了算法名称的X。

考虑第5步,是充分展现dancing link 巧妙的操作了。这一点不编程调试,是不会了解其中的巧妙的。就像是dancing link 表的构造一样,原论文并没有给出方法,但是确实要考虑一下如何构造dancing link。为了简单说明,我们提醒大家注意:在删除列和行操作上,dancing link 的删除是保持列和行的结构的情况下,整块的从dancing link上删除掉的,之所以保持原先的结构下整块的删除,是为了保证我们可以方便的回到原先的状态,下面就是删除的一个示意图:

 


图片

值得注意的是,上述灰色区域是整块删除的,也就是说,我们的C列中的元素仍然保持着原先的上下链接关系,而那灰色的两行仍然保持着左右的链接关系,同时这些元素其上下左右的指针域仍然有效。方便他们回到dancing link中去。至于为什么叫做dancing link,knuth声称这个一系列巧妙的操作想一个舞者跳着优雅的舞步,不过在编完程序后,我觉得,其实上述操作很类似于我们过去玩的泡泡堂游戏。另外,这个十字链表也非常像我们的跳舞毯:


图片

操作的细节就是那个奇怪的操作了:L[R[x]] = L[x]; R[L[x]] =R[x]; 恢复操作则是,R[L[x]]=x ; L[R[x]]= x;参见knuth的原论文和dancing link在搜索中的应用一文。这是dancing link最神奇的操作,因为它有点反常规的思路,为什么又把删去的有变回来呢?因为回溯的需要,要恢复预先的状态。在常规编程中是忌讳的操作,反而在这个算法框架中发挥了奇效。

除了上述的两个原因,还有一个尚未得到证明的结论,dancing link先搜索那些非零元素最少的列(如上图就是{1,2,3,5}中的任意一个列,即对应Algorithm X的2步),以下是一个扩展非零元较少的列和较多非零元的搜索树情况:

root —|— node —— ......

           |— node —— ......

扩展含两个非零元的列的搜索树

root —|— node —— ......

           |— node —— ......

           |— node —— ......

           |— node —— ......

扩展含四个非零元的列的搜索树

这一点,我猜测是因为,越是扩展节点少的列,生成的节点树越高,但同时,一旦被剪枝,则潜在被去除的节点数目要很大。往往节点树随着扩展的深度大量的减小。knuth通过实验说明这个想象,但理论上的证明还没有给出。

dancing link 就简单的介绍,这仅仅是一个在不重复的覆盖问题上的应用:也就是包含两个硬约束:

满足性和唯一性,

这两个约束方便我们高效的剪枝,但是在实际生活中,我们往往遇到不完全的覆盖和可重复覆盖问题,如选址问题等等,显然,我们的硬约束编成了一个柔性的约束条件(使用0-1规划的语言):

尽量保证在选取较少的行,这些行使得非空列的数目占总列数至少超过给定的比例(一个列可以有很多的非零元素,不要求仅有一个非零项)。这一问题可以称之为可重复的不完全覆盖问题。现在,正是我在考虑的问题,我想在Dancing link(也就是 algorithm X)的框架上,给出这个问题的解。

                                                                                                                                                        2011-4-16

 补充 2011-4-25

在TAOCP中有一章节讲述可用正交循环表存储稀疏矩阵。直觉上感觉有一些相关的东西。这里简单的提一下,参见TAOCP 2.2.6。其中正交循环表的结构为:

struct node*{

node* up, *left;

int row,col;

int value;

}

 因为少了Dancing link对矩阵的修改,而只是为了快速的对矩阵进行访问和定位,所以正交循环表只采用单向链表结构,所以并不需要right以及down指针。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值