舞蹈链 DLX
欢迎访问——该文出处-博客园-zhouzhendong
去博客园看该文章--传送门
舞蹈链是一个非常玄学的东西……
问题模型
精确覆盖问题:在一个01矩阵中,是否可以选出一些行的集合,使得在这些行的集合中,每列有且仅有1个1。
例子
1 1 0 1 0 1 0 0 0
1 0 1 0 1 0 1 1 0
0 1 1 0 1 1 1 1 1
1 0 0 0 1 0 0 0 0
0 1 0 1 0 0 0 0 0
0 0 0 0 0 1 0 0 1
那么答案就是
重复覆盖问题:精确覆盖问题的变形,允许列中的1多于1个。
具体算法见后面。
X算法
解决这个精确覆盖问题,我们首先有一个X算法。
X算法基于dfs,具体是这样的:每次,对于当前剩余矩阵(一开始就是原矩阵),然后选择一列,这一列为当前需要匹配的列(随便选,应每一列都要匹配),然后对于当前列,选择一个剩余矩阵中存留的行,使得该行的这一列为1,使得该列得以覆盖,然后删除会有冲突的一些情况,具体为:
1、删除该行的其他地方的1所覆盖的列
2、删除该列暂未被选中的其他行
举个例子:
比如选中第6列第6行
那么删除相斥的行(蓝色行)和相关的列(红色的列)剩余矩阵为:
1 0 1 0 1 1 1
1 0 0 0 1 0 0
0 1 0 1 0 0 0
然后,比如说再删除剩余矩阵的第5列的第1行,那么:
剩余的矩阵为:
1 1
最后一步,删除所有的即可。这就找到了一组解。
当然会有寻解失败的情况,所以是回溯的算法。
舞蹈链 - DLX 算法 概述
上面的X算法好是好,但是空间问题太严重,建立剩余矩阵也太耗时,所以我们采用舞蹈链优化的X算法—— DLX算法 来解决问题!
DLX什么鬼??
DLX 算法基于 X 算法和十字链表,其原理就是把转换矩阵的过程通过链来实现。
先贴一个生动的图:
来自<万仓一黍-博客园>
注意这个图还是稍微有点难以描述的地方:DLX算法的链是循环的,图片难以做到这个效果,用问题描述就是在上图的基础上,每行的尾部再连向首部,每列也是如此。
那么,有了这个结构,在删除的时候,就只需要修改某些链即可。
比如上图,如果要删除第2列,先删掉,然后枚举选择哪一行,如果是第3行,那么只要把该行的除该列位置以外的元素以及这些元素的列都从链表中删除即可,对于删除列,改变的是横向的第一行的链表,对于删除单个元素,仅仅改变纵向的链表;也就是说横向的指针除了第一行,其他的行都会被改变。
这个时候,十字链表循环的原因就慢慢出来了。
当选择一列的时候,这一列不一定就是第一列,要删除该某行元素,起点不一定是第一行,循环链表解决的这个问题。
在实际实现中,为了优化DLX的速度,往往会维护每列的元素个数,每次选择列的时候选择元素个数最小的那一列,据说这样会快一点。
说到这里可能读者还是一头雾水,没关系,结合代码来看,慢慢体会即可。
至于DLX解决重复覆盖问题,我们只要在删除列的时候,不要删光涉及它的行就可以了。
实现
DLX算法大致懂了之后,看起来挺简单的,实际上如果不看标算,一开始写的也是一头雾水。
首先定义一下:
x[i]表示节点i的行号
y[i]表示节点i的列号
L[i]表示节点i的左指针指向的节点编号
R[i]表示节点i的右指针指向的节点编号
U[i]表示节点i的上指针指向的节点编号
D[i]表示节点i的下指针指向的节点编号
C[i]表示第i列的元素个数
ans[i]表示答案集合中的第i行的行号
ansd表示答案集合行数
n,m表示总行数和总列数
cnt表示元素总数
DLX模板,首先是初始化。
初始化的时候,我们要知道有多少列(比如说m),然后构建第一个虚点和m个列首元素。同时初始化DLX里面的已有元素个数,即m+1。注意是构建循环的链表。
void init(int c){ memset(x,0,sizeof x),memset(y,0,sizeof y); memset(L,0,sizeof L),memset(R,0,sizeof R); memset(U,0,sizeof U),memset(D,0,sizeof D); memset(C,0,sizeof C),memset(ans,0,sizeof ans); anscnt=0,m=c; for (int i=0;i<=m;i++) L[i]=i-1,R[i]=i+1,U[i]=D[i]=i; L[0]=m,R[m]=0,cnt=m; }
然后是建立十字链表
然而我们会发现这个过程远远比我们想象中的复杂。不用想了,看下面的解说。
在建立十字链表的时候,我们一行一行来,对于同一行,一列一列来。对于同一行的L和R,我们发现每行的相邻两个元素(这里说的元素是只选1的)的L等于下一个元素,R等于上一个元素,当然存在特殊情况:每行的第一个元素的L和最后一个元素的R,这样的话,只要在一行开始前记录一下行首标记,然后处理完一行之后再对首尾元素特殊赋值即可。当然如果一行没有元素,不可进行这个特殊赋值的操作,会出错,L和R以及行的处理,这里就写到这里,具体代码可以到练习题里面任意的题目里面联系代码理解。
现在还剩下U和D的问题。D好对付,就是当前列的首元素(循环的嘛),那么U呢??难不成每列搞一个数组之类的东西……,然后……???你硬要这样我也不拦你……对于一个简洁的代码来说,这是冗余!!!
我们发现,在修改U[该列]之前,U[该元素]=U[该列]!!这是个好东西,一发修改了D[U[该列]]以及U[该元素]之后,在修改D[该元素]以及U[该列]也不迟。
那不就好了,具体见代码。别忘了给该列的计数器C[该列]+1。
void link(int i,int j){ cnt++; x[cnt]=i; y[cnt]=j; L[cnt]=cnt-1; R[cnt]=cnt+1; D[cnt]=j; D[U[j]]=cnt; U[cnt]=U[j]; U[j]=cnt; C[j]++; }
然后就是最最重要也是最最玄学的暴力部分了!!!
很简单,每次找到C最小的一列,然后删除这一列以及相应的行,然后枚举要删除这一列得先删除哪些行。然后枚举是取哪一行的使得该列被精确覆盖,对于这一行覆盖的其他列,也分别进行删除操作。然后继续在剩余矩阵中求解。直到没有列剩余为止。别忘了恢复链表。注意这个过程有个很奇怪的地方:请在删除操作的地方按照R走,在恢复的时候按照L走,不知道为什么这样比较快……我有同学因为这个写成同向的,导致NOIP2009靶形数独卡了很久……
void Delete(int k){ L[R[k]]=L[k]; R[L[k]]=R[k]; for (int i=D[k];i!=k;i=D[i]) for (int j=R[i];j!=i;j=R[j]){ U[D[j]]=U[j]; D[U[j]]=D[j]; C[y[j]]--; } } void Reset(int k){ L[R[k]]=k; R[L[k]]=k; for (int i=U[k];i!=k;i=U[i]) for (int j=L[i];j!=i;j=L[j]){ U[D[j]]=j; D[U[j]]=j; C[y[j]]++; } } bool solve(){ if (R[0]==0) return true; anscnt++; int k=R[0]; for (int i=R[k];i!=0;i=R[i]) if (C[i]<C[k]) k=i; Delete(k); for (int i=D[k];i!=k;i=D[i]){ ans[anscnt]=x[i]; for (int j=R[i];j!=i;j=R[j]) Delete(y[j]); if (solve()) return true; for (int j=L[i];j!=i;j=L[j]) Reset(y[j]); } Reset(k); anscnt--; return false; }
解释完了,贴膜板
const int N=100+5,M=100+5,S=N*M; struct DLX{ int n,m,cnt; int x[S],y[S],L[S],R[S],U[S],D[S]; int C[M],anscnt,ans[N]; void init(int c){ memset(x,0,sizeof x),memset(y,0,sizeof y); memset(L,0,sizeof L),memset(R,0,sizeof R); memset(U,0,sizeof U),memset(D,0,sizeof D); memset(C,0,sizeof C),memset(ans,0,sizeof ans); anscnt=0,m=c; for (int i=0;i<=m;i++) L[i]=i-1,R[i]=i+1,U[i]=D[i]=i; L[0]=m,R[m]=0,cnt=m; } void link(int i,int j){ cnt++; x[cnt]=i; y[cnt]=j; L[cnt]=cnt-1; R[cnt]=cnt+1; D[cnt]=j; D[U[j]]=cnt; U[cnt]=U[j]; U[j]=cnt; C[j]++; } void Delete(int k){ L[R[k]]=L[k]; R[L[k]]=R[k]; for (int i=D[k];i!=k;i=D[i]) for (int j=R[i];j!=i;j=R[j]){ U[D[j]]=U[j]; D[U[j]]=D[j]; C[y[j]]--; } } void Reset(int k){ L[R[k]]=k; R[L[k]]=k; for (int i=U[k];i!=k;i=U[i]) for (int j=L[i];j!=i;j=L[j]){ U[D[j]]=j; D[U[j]]=j; C[y[j]]++; } } bool solve(){ if (R[0]==0) return true; anscnt++; int k=R[0]; for (int i=R[k];i!=0;i=R[i]) if (C[i]<C[k]) k=i; Delete(k); for (int i=D[k];i!=k;i=D[i]){ ans[anscnt]=x[i]; for (int j=R[i];j!=i;j=R[j]) Delete(y[j]); if (solve()) return true; for (int j=L[i];j!=i;j=L[j]) Reset(y[j]); } Reset(k); anscnt--; return false; } }dlx;
应用举例
数独求解--传送门
练习题
模板题
POJ3740 传送门 题解
正常题
POJ2676 传送门 题解
POJ3074 传送门 题解
POJ3076 传送门 题解
NOIP2009T4 传送门 题解
骨灰题
BZOJ1501 [NOI2005]智慧珠游戏 传送门 题解