Dancing links
基本思路(Main Thoughts):
Dancing link是一种十分优美的数据结构。
通常配合IDA*,二分等方法解决可以转化为精确覆盖和重复覆盖的题目。
精确覆盖:在一个01矩阵中选几行,使得这几行组合起来的矩阵每列有且只有一个1
重复覆盖:每列可以有多个1
实现步骤(Implementation Steps):
如果是正常的搜索
解决步骤:
- 选最左边第一个没有被删除的列
- 选择一个在1中选出的列中有1的行,其他行直接删除
- 删掉2中选出的行剩余的元素所在的列
- 重复1,直到有一次没有可选列
- 如果一列没有被删除且列中已经没有元素,则回溯重新选择一行
DLX基本上用到的就是这种思想
辅助数组:
R,L,U,D代表当前元素在链表中右左上下的元素
C代表i点所在列
S代表i列元素个数
F,last,Last用于建表,分别是i行第一个元素,i行上一个元素,i列上一个元素。
执行步骤:
1、Dancing函数的入口
2、判断Head.Right=Head?,若是,输出答案,返回True,退出函数。
3、获得Head.Right的元素C
4、标示元素C
5、获得元素C所在列的一个元素
6、标示该元素同行的其余元素所在的列首元素
7、获得一个简化的问题,递归调用Daning函数,若返回的True,则返回True,退出函数。
8、若返回的是False,则回标该元素同行的其余元素所在的列首元素,回标的顺序和之前标示的顺序相反
9、获得元素C所在列的下一个元素,若有,跳转到步骤6
10、若没有,回标元素C,返回False,退出函数。
由于篇幅有限,在此贴出大神blog : http://www.cnblogs.com/grenet/p/3145800.html
以供大家参考。
模板(Code):
//感谢诚叙同学的修改和注释 http://www.cnblogs.com/Robert-Yuan/
1 void build(){ //初始化 2 /*构造矩阵第一行: 3 1.将第一行的两端环状处理。[提示]:第一行的元素的标号就是其列的标号。 4 2.对于第一行的所有元素: 5 连接其左右;标注其行列;清空列中表示的元素个数;清空表示列的上一个元素的d数组。*/ 6 7 L[0]=m,R[m]=0; 8 for(int i=1;i<=m;i++){ 9 L[i]=i-1,R[i-1]=i; 10 c[i]=i;r[i]=0; 11 s[i]=0; 12 d[i]=0; 13 } 14 15 //清空所有行上的表示上一个元素的last数组和表示首元素的f数组 16 for(int i=1;i<=n;i++) last[i]=f[i]=0; 17 } 18 19 void del(int col){ //删列操作 20 /* 21 1.在第一行中删除这个列节点。 22 2.找到每个在这条列上的节点i 23 将节点i所在的行从列表中删除 24 */ 25 26 L[R[col]]=L[col],R[L[col]]=R[col]; //在第一行中删除这个列节点。 27 28 for(int i=D[col];i!=col;i=D[i]) 29 for(int j=R[i];j!=i;j=R[j]) //将节点i所在的行删除干净[因为是环状的,所以可以删掉这行上的所有节点] 30 U[D[j]]=U[j],D[U[j]]=D[j],s[c[j]]--; //这些节点都在列表中不再被查询得到。 31 } 32 33 void add(int col){ //恢复列操作 34 /* 35 1.在第一行中恢复这个列节点。 36 2.找到每个在这条列上的节点i 37 将节点i所在的行有顺序的从列表中恢复 38 */ 39 40 R[L[col]]=col,L[R[col]]=col; //因为删除时其实是间接删除[将其两边元素的指针改变],恢复只需要从它自己出发恢复两边即可。 41 42 for(int i=U[col];i!=col;i=U[i]) 43 for(int j=L[i];j!=i;j=L[j]) //同删除的顺序相反的添加回来[即后删去的先添加],才保证了顺序,不会错 44 U[D[j]]=j,D[U[j]]=j,s[c[j]]++; //同删除的感觉,通过行表找到这些丢失的点,然后从它自己来恢复列表中它们的位置。 45 } 46 47 bool search(int k){ 48 if(R[0]==0){ //第一行中的所有元素都被删除[即列都被标记,完成了精确覆盖]了 49 printf("%d",k); 50 for(int i=1;i<=k;i++) 51 printf(" %d",r[ans[i]]); 52 putchar('\n'); 53 return true; 54 } 55 int Min=INF,C; 56 for(int i=R[0];i;i=R[i]) 57 if(Min>s[i]) Min=s[i],C=i; //优先选择列中元素少的列来处理 58 del(C); 59 for(int i=D[C];i!=C;i=D[i]){ //确定要删掉这列,但是要考虑删掉哪一行[当然是必须要与这一列相交的行] 60 ans[k+1]=i; 61 for(int j=R[i];j!=i/*环状链表的好处,可以循环寻找,找到所有元素*/;j=R[j]) del(c[j]); //当确定删掉i行时,还要删掉所有与其相交的列 62 if(search(k+1)) return true; 63 for(int j=L[i];j!=i;j=L[j]) /*环状链表的另一个好处,可以让添加的顺序与删除的顺序相反,从而达到后删去的先添加的形式*/ 64 add(c[j]); //回溯过程,补充回原来删去的列。 65 } 66 add(C); 67 return false; 68 } 69 70 void link(int row/*行*/,int col/*列*/){ 71 size++; //点的数目++ 72 s[col]++; //所在列的元素个数++ 73 74 /*行操作: 75 如果这一行之前没有元素了,这个元素作为本行的首元素,记录在f[row]中;并记录在表示上一个元素的last[row]中。 76 若所在行之前还有元素,将这个元素与上一个元素连起来,更新last[];并将这个元素的右端与该行的首元素连起来,形成一个环状的结构。*/ 77 if(!last[row]) 78 last[row]=size, 79 R[size]=size,L[size]=size, 80 f[row]=size; 81 else 82 L[size]=last[row],R[last[row]]=size, 83 last[row]=size, 84 R[size]=f[row],L[f[row]]=size; 85 86 /*列操作: 87 【与行操作有所不同】:我们事先已经构造出了矩阵的第一行,不存在所谓首元素,因为首元素就是这一列的标号。 88 如果这一列之前没有添加过元素,将当前元素与这一列的标号首尾相连,更新表示上一个元素的d[col]。 89 若之前添加过元素,将这个元素与上一个元素连起来;并将这个元素充当此行的末尾,与这一列的标号相连构成一个环状结构;更新d[col]。*/ 90 if(!d[col]) 91 U[col]=size,D[size]=col, 92 D[col]=size,U[size]=col, 93 d[col]=size; 94 95 else 96 U[col]=size,D[size]=col, 97 U[size]=d[col],D[d[col]]=size, 98 d[col]=size; 99 100 c[size]=col,r[size]=row; //处理完与其它节点的关系后,再给自己打上行列的标号 101 }
时间&空间复杂度(Time & Memory Complexity):
空间:O(?)
时间:O(?)
主要用途&优缺点(Main Applications & Advantages & Disadvantages):
主要用途: 通常配合IDA*,二分等方法解决可以转化为精确覆盖和重复覆盖的题目。
优点:快
缺点:长
推荐题目&数据(Recommendatory Problems & Data) :
Hustoj 1017 裸的精确覆盖
UVA 1603 破坏正方形 DLX重复覆盖