1.概述
Dacing Links (DLX) 算法是Donald Knuth [2]提出,用以解决精确覆盖(exact cover)问题,是X算法在计算机上的优化。
1.1 精确覆盖问题
所谓精确覆盖,是指两两不相交的子集的集合,这些子集的并集可以得到全集。完整的定义 [1]如下:
在一个全集X中若干子集的集合为S,精确覆盖是指,S的子集S*,满足X中的每一个元素在S*中恰好出现一次。
举例:令 S = {N, O, E, P} 是集合X = {1, 2, 3, 4}的一个子集,并满足:
N = { }
O = {1, 3}
E = {2, 4}
P = {2, 3}.
其中一个子集 {O, E} 是 X的一个精确覆盖,因为 O = {1, 3} 而 E = {2, 4} 的并集恰好是 X = {1, 2, 3, 4}。同理, {N, O, E} 也是 X 的一个精确覆盖。
用关系矩阵来表示S的每个子集与X的元素之间包含关系,矩阵每行表示S的一个子集,每列表示X中的一个元素。矩阵行列交点元素为1表示对应的元素在对应的集合中,不在则为0。
精确覆盖问题转化成了求矩阵的若干个行的集合,使每列有且仅有一个1。S* = {B, D, F} 便是一个精确覆盖。
1.2 双向十字链表
实现DLX算法的数据结构是双向十字链表,现在先简单介绍一下双向十字链表。
双向十字链表用LRUD来记录,LR来记录左右方向的双向链表,UD来记录上下方向的双向链表。比如,对6*7矩阵
用双向十字链表可以表示如下:
其中,h代表总的头链表head,ABCDEFG为列的指针头。
双向十字链表可以用数组来加以模拟。对4*4的01矩阵([4]中的一个例子)
1 0 0
0 0 1
1 1 1
0 1 0
LRUD的双向十字链表结构如下:
其中,把头节点head编号为0,列分别编号为1,2,3,4。第一行的两个1编号为5,6,第二行的一个1编号为7,第三行三个1编号为8,9,10。第四行两个1,编号为11,12。编号的顺序都是从左到右。1列的下一个节点就是编号为5的1,编号为5的1的下面又是编号11的1,编号为5的1的左边和右边都是编号为6的1。
1.3 DLX算法描述
对精确覆盖问题,容易想到一个启发式的递归算法:(1)选中关系矩阵A的列c,则满足A(i, c)=1的行i均不可用,删除列c与所有的行i;(2)对选中的列c,选中行r满足A(r, c)=1;则满足A(r, j)=1的列j也均不可用,删除行与所有的列j;(3)对删除后的A进行递归(1)(2)处理。
上述非确定算法即是X算法,伪代码如下:
如果A是空的,问题解决;成功终止。
否则,选择一个列c(确定的)。
选择一个行r,满足 A[r, c]=1 (不确定的)。
把r包含进部分解。
对于所有满足 A[r,j]=1 的j,
从矩阵A中删除第j列;
对于所有满足 A[i,j]=1 的i,
从矩阵A中删除第i行。
在不断减少的矩阵A上递归地重复上述算法。
对X算法的优化一:在X算法的步骤(2)中选择的行r有可能是错的,为了减少递归次数,则需要回溯。为了便于X算法中有查找、删除等操作以及回溯,可采用双向十字链表。假设x 指向双向链的一个节点;L[x] 和R[x] 分别表示x 的前驱节点和后继节点。每个程序员都知道如下操作:
L[R[x]] ← L[x], R[L[x]] ← R[x] (1)
将x 从链表删除的操作。但是只有少数程序员意识到如下操作:
L[R[x]] ← x, R[L[x]] ← x (2)
是把x重新链接到双向链中。关于操作(2)的研究促使Knuth写了论文[2],操作(2)为了回溯用的,也正是DLX算法的精髓。
对X算法的优化二:在选择列c时,应选择的是A中所有列中1元素最少的一列。至于为什么选择最少的一列,不在本文讨论之列。如果去掉优化二,写的代码很有可能TLE。
为建立关系矩阵A的双向十字链表、加快运行速度。对每一个对象,记录如下几个信息:
- L[x], R[x], U[x], D[x], C[x];LR来记录左右方向的双向链表,UD来记录上下方向的双向链表;C[x]是指向其列指针头的地址,即表示x所在的列。
- head指向总的头指针,head通过LR来贯穿的列指针头。
- 每一列都有列指针头。行指针头可有可无,为了更方便地建立左右方向的双向链表,加个行指针头还是很有必要的。
- 另外,开两个数组S[x], O[x];S[x]记录列链表中结点的总数,O[x]用来记录搜索结果。
DLX算法的伪代码如下:
其中,R[h]=h即表示A为空,cover column操作即为X算法中步骤(1),uncover colunm操作即为回溯。关于DLX算法的演示过程请参看[6]。
DLX算法的C代码:
[cpp] view plain copy
1. /*remove column c and all row i that A(i,c)==1*/
2. void re_move(int
3. {
4. int
5. //remove column c
6. R[L[c]]=R[c];
7. for(i=D[c];i!=c;i=D[i]) //remove row i that (i,c)==1
8. for(j=R[i];j!=i;j=R[j])
9. {
10. U[D[j]]=U[j];
11. D[U[j]]=D[j];
12. //decrease the count of column C[j]
13. }
14. }
15.
16. /*backtrack, resume*/
17. void resume(int
18. {
19. int
20. for(i=U[c];i!=c;i=U[i])
21. for(j=L[i];j!=i;j=L[j])
22. {
23. S[C[j]]++;
24. U[D[j]]=j;
25. D[U[j]]=j;
26. }
27. L[R[c]]=c;
28. R[L[c]]=c;
29. }
30.
31. int dfs(int
32. {
33. int
34. if(R[0]==0) return 1; //the matrix A is empty
35.
36. for(i=R[0];i!=0;i=R[i]) //select the column c which has the fewest number of element
37. if(S[i]<min)
38. {
39. min=S[i];
40. c=i;
41. }
42. re_move(c);
43.
44. for(i=D[c];i!=c;i=D[i])
45. {
46. //record the result
47. for(j=R[i];j!=i;j=R[j])
48. re_move(C[j]);
49.
50. if(dfs(depth+1)) return
51.
52. for(j=L[i];j!=i;j=L[j]) //backtrack
53. resume(C[j]);
54. }
55.
56. resume(c);
57. return
58. }
3. 问题
3.1 POJ 3740
用到了行指针头H[ ],以建立左右方向的双向链表,采用的是头插法。
O[ ] H[ ]数组开成了16, TLE了3次。O[ ] 应该开成最多列数300,H[ ]应该开成17。
源代码:
Accepted | 212K | 266MS | C | 1665B | 2013-10-24 22:14:13 |
[cpp] view plain copy
1. #include "stdio.h"
2. #include "string.h"
3.
4. #define MAX 5000
5.
6. int
7. int
8.
9. /*remove column c and all row i that A(i,c)==1*/
10. void re_move(int
11. {
12. int
13. //remove column c
14. R[L[c]]=R[c];
15. for(i=D[c];i!=c;i=D[i]) //remove row i that (i,c)==1
16. for(j=R[i];j!=i;j=R[j])
17. {
18. U[D[j]]=U[j];
19. D[U[j]]=D[j];
20. //decrease the count of column C[j]
21. }
22. }
23.
24. /*backtrack, resume*/
25. void resume(int
26. {
27. int
28. for(i=U[c];i!=c;i=U[i])
29. for(j=L[i];j!=i;j=L[j])
30. {
31. S[C[j]]++;
32. U[D[j]]=j;
33. D[U[j]]=j;
34. }
35. L[R[c]]=c;
36. R[L[c]]=c;
37. }
38.
39. int dfs(int
40. {
41. int
42. if(R[0]==0) return 1; //the matrix A is empty
43.
44. for(i=R[0];i!=0;i=R[i]) //select the column c which has the fewest number of element
45. if(S[i]<min)
46. {
47. min=S[i];
48. c=i;
49. }
50. re_move(c);
51.
52. for(i=D[c];i!=c;i=D[i])
53. {
54. //record the result
55. for(j=R[i];j!=i;j=R[j])
56. re_move(C[j]);
57.
58. if(dfs(depth+1)) return
59.
60. for(j=L[i];j!=i;j=L[j]) //backtrack
61. resume(C[j]);
62. }
63.
64. resume(c);
65. return
66. }
67.
68. void
69. {
70. int
71. for(i=1;i<=n;i++) //初始化列的指针头
72. {
73. L[i]=i-1; R[i]=i+1;
74. U[i]=i; D[i]=i;
75. C[i]=i;
76. }
77. L[0]=n; R[0]=1;
78. R[n]=0;
79.
80. sizeof(H));
81. sizeof(S));
82. count=n+1;
83.
84. for(i=1;i<=m;i++)
85. for(j=1;j<=n;j++)
86. {
87. "%d",&temp);
88. if(!temp) continue;
89.
90. if(H[i]==-1) //为行i的第一个非零元素
91. H[i]=L[count]=R[count]=count;
92. else
93. {
94. //连接同一行的左右节点
95. R[L[H[i]]]=count; L[H[i]]=count;
96. }
97.
98. //连接同一列的上下节点
99. D[U[j]]=count; U[j]=count;
100. //该节点属于列j
101. S[j]++;
102. count++;
103. }
104. }
105.
106. int
107. {
108. while(scanf("%d%d",&m,&n)!=EOF)
109. {
110. init();
111. if(dfs(0))
112. "Yes, I found it\n");
113. else
114. "It is impossible\n");
115. }
116. return
117. }