DancingLinksX|精确覆盖&重复覆盖
DancingLinksX|精确覆盖&重复覆盖
本文介绍DLX及其在精确覆盖和重复覆盖问题中的应用。
在DLX之前先了解精确覆盖和重复覆盖。
精确覆盖
给出一个仅有0、1的矩阵,选择其中的一些行使得这些行组成的集合中每一列都有且仅有一个1,求方案的存在性及具体方案。
重复覆盖
给出一个仅有0、1的矩阵,选择其中的一些行使得这些行组成的集合中每一列都至少有一个1,求方案的存在性及具体方案。
Algorithm X & DLX
精确覆盖和重复覆盖都是NP完全问题,所以目前解决方法只有搜索。
在精确覆盖和重复覆盖的问题中,如果尝试选择某一行包括至解集中,那么其他的一些行就不能再选择,一些列可以删去。这样就得到了一个更简单的矩阵,于是可以用递归的搜索算法解决。
Algorithm X就是直接通过回溯解决精确覆盖和重复覆盖问题。
解决精确覆盖的伪代码如下:
如果A是空的,问题解决;成功终止。
否则选取一列c,
枚举当前列中的1,位置设为(r,c)
取r尝试作为解集的一部分。
对于r行中每个1对应的列(包括c),它们已经在解集中有1了,
它们可以不用再考虑了;而且每列有且仅有一个1,必须删除它们防止造成冲突。
而在这些列中有1的行会与r冲突,所以还有删除在这些列及c中有1的行。
递归。
回溯。
实际代码中,由于每次都会删除当前列c,所以一般在枚举之前先删除c。
需要指出的是,上面选择列c的操作中,选择哪一列并不会对答案的存在性产生影响,因为只有在所有列都被删除的时候算法才会终止。
重复覆盖:
由于重复覆盖并不要求每列有且仅有一个1,所以要对X算法做一些小改动。
如果A是空的,问题解决;成功终止。
否则选取一列c,
枚举当前列中的1,位置设为(r,c)
取r尝试作为解集的一部分。
对于r行中每个1对应的列(包括c),它们已经在解集中有1了;
所以可以删除它们以简化问题。
递归。
回溯。
但是,在一般的写法中,X算法在运行过程中需要不断地更改并重新生成新的矩阵,耗费了大量时间。这时将要介绍的DancingLinks就发挥用处了,因为它在删除和恢复行列上消耗的时间很少。
DLX,就是用DancingLinks实现的X算法。
优化
考虑到上面选择列时,无论选择哪一列最终都能得出解,那么选择哪一列就是一个值得思考的问题。下面介绍一种推荐的策略:选取元素个数列最少的一个。这种策略基于这样的考虑:在元素少的列枚举行的次数更少。
其实我也不知道有没有快
DancingLinks
DancingLinks是 Donald E.Knuth(Stanford University)提出的一种数据结构,也可以称为双向循环交叉十字链表(双向循环二维链表)。
普通的链表支持O(1)删除/恢复节点(添加操作暂不提),而类比之DancingLinks可以O(n)删除/恢复 行/列。其中n为二维链表的边长。由于在执行这些操作时节点间指针的修改等好像节点手拉手跳舞,DancingLinks的这个别名才流行起来。
那么接下来介绍DancingLinks。
结构
先上图
为了更好地组织节点,一般在数据节点(图中用阿拉伯数字标出的节点)之外定义一些辅助节点,包括:
总头节点(head):用以判断链表是否为空。当链表为空时,有r[head]=head。
列首节点(col[]):连接在总头节点的右边,并垂直连接每一列的节点。
通过辅助节点和数据节点,可以访问结构中的所有数据。
每个节点包括四个指针:l,r,u,d。分别指向左右上下四个方向相邻的节点。
注意 由于DancingLinks是循环链表,所以最左边的节点的左指针应该指向这一列最右边的节点,其他边缘的节点同理。u[head]和d[head]一般不使用。
有了这样的结构,就可以通过head->col[y]->x的方式访问每一个数据节点。
在结构的基础上,就可以实现一些操作以满足算法的需求了。
下面是代码实现定义,还有一些细节的地方会写在注释里。
有一些上面没有提到的辅助变量或者数组,后面的操作会用得到。
!!!注意!!! 需要指出的是,代码中的col[i]是第i列的列首节点id,而row[i]是第i行第一个数据节点的id,注意区别!
|
|
操作
初始化
|
|
不废话。大概就是新建总头结点,列首节点,同时将他们连起来。
注意:如果一个节点在某个方向上没有相邻的节点,则它这个方向的指针要指向自己。
节点
插入
|
|
删除/恢复
如果想要真正删除一个点,可以将上下左右的点的指针互相连起来(类似缝补)。但是在DLX里,这样的操作没有多大意义。而且这样删除后这个节点就无法访问到了。为了实现节点的删除后恢复操作,一般只使用左右删除和上下删除。
注意:在删除和恢复时节点本身的指针不需要更改,只需要更改周围节点的指针。
|
|
行&列
删除/恢复
对于一行或列的删除操作,只要选取一个起始点,然后像下面一样操作(以行为例):
|
|
但是需要注意的是,这个操作之后节点x并没有被删除。同之前提到的原因一样,这是为了在以后需要的时候能够通过节点x恢复这一行。
|
|
另外,对于列的删除操作,由于列拥有列首节点,所以在删除时一般选择列首节点作为x。而列首节点可以通过col[y]来访问到,又由于判断表空用的是r[head]=head,所以在删除列的时候需要delr(列首节点)。
应用
一个说明
首先说明一点:一个已经被删除的节点不能在相同方向上再被删除一次。
证明(以左右删除为例):设x为已删除节点,在第二次删除x时,所执行的操作是:
int ll=l[x],rr=r[x]; r[ll]=rr,l[rr]=ll;
问题的根源在于,由于x已经被删除,l[x]和r[x]存放的是x上一次被删除时它相邻的节点。在第二次删除时,可能相邻的节点已经发生变动,这样再进行“r[ll]=rr,l[rr]=ll;”就可能错误地修改指针使结构混乱。
DLX在精确覆盖中的应用
在精确覆盖中,只要将X算法用DancingLinks实现即可。下面要讲的是一些代码中的细节。
代码如下:
|
|
代码中的deCol(c)表示删除c所在的列,同时将在c列中有1的行删除。
而deRow(r)则表示选择并删除r所在行,同时删除r行中所有1对应的列。
注意:DeCol中只需要delr(列首节点),而不能delr(其他该列的节点),否则下面删除行的时候无处开始。
实例
数独、N皇后
DLX在重复覆盖中的应用
在精确覆盖中,由于要删除的每一列中有1的行都已经被删除了,所以只要delr(列首节点)也不会造成问题,而且因为要枚举行,所以不能delr(其他节点)。
但是在重复覆盖中,形式发生了一些变化。由于删除列的时候不需要删除对应的行,所以在删除列时如果不delr(节点),在选取其他行时可能会遍历到这一列,根据前面的说明,会导致这个节点被重复删除而出现问题。
于是改变策略,先枚举后删除。为了防止出现上面的问题,在删除时从当前节点开始枚举,即不删除当前节点。
|
|
然而由于重复覆盖在每一次选择后能够减小的问题规模有限,其速度比相同输入规模的精确覆盖慢很多。我们需要一些优化。
剪枝
一次递归至少可以删除一列,所以可以估计还要删除多少列来剪枝。
一个想法是通过精确覆盖+贪心剪枝。
大概如下:
初始化当前每列为未删除。
枚举每一列:
如果标记为未删除:
cnt++;
标记该列为删除;
同时将这一列中每一行对应的列都标记为删除。
cnt就是估计值。
代码:
|
|
例题
hust1017,poj3740(精确覆盖template)
hdu3498(重复覆盖template)
hdu2295(二分+重复覆盖)
zoj3209(精确覆盖)
fzu1686(重复覆盖)
fzu2165(最小花费)
参考和应用
http://www.cnblogs.com/grenet/p/3145800.html
http://blog.csdn.net/mu399/article/details/7627862
https://www.cnblogs.com/-sunshine/p/3358922.html