【算法】Dancing Links (DLX) I

From: https://i-blog.csdnimg.cn/blog_migrate/4903ef89996e78626602f4a4968f74bf.png

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 1  0 0

0 0  0 1

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代码:

  1. /*remove column c and all row i that A(i,c)==1*/  
  2. void re_move(int c)  
  3. {  
  4.     int i,j;  
  5.     L[R[c]]=L[c];                      //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.             S[C[j]]--;                 //decrease the count of column C[j]  
  13.         }  
  14. }  
  15.   
  16. /*backtrack, resume*/  
  17. void resume(int c)  
  18. {  
  19.     int i,j;  
  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 depth)  
  32. {  
  33.     int i,j,c,min=20;  
  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.         O[depth]=i;                     //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 1;  
  51.   
  52.         for(j=L[i];j!=i;j=L[j])         //backtrack  
  53.             resume(C[j]);  
  54.     }  
  55.   
  56.     resume(c);  
  57.     return 0;  
  58. }  



2. Referrence


[1] 维基百科,精确覆盖问题.

[2] Donald Knuth, Dancing Links.

[3] 吴豪,隋清宇(sqybi),Dancing Links中文版.

[4] momodi, Dancing Links在搜索中的应用.

[5] mu399,简单易懂的Dancing links讲解(1).

[6] mu399,简单易懂的Dancing links讲解(2).


3. 问题


3.1 POJ 3740


用到了行指针头H[ ],以建立左右方向的双向链表,采用的是头插法。


O[ ] H[ ]数组开成了16, TLE了3次。O[ ] 应该开成最多列数300,H[ ]应该开成17。


源代码:

3740Accepted212K266MSC1665B2013-10-24 22:14:13
  1. #include "stdio.h"  
  2. #include "string.h"  
  3.   
  4. #define MAX 5000  
  5.   
  6. int L[MAX],R[MAX],U[MAX],D[MAX],C[MAX],S[300],O[300],H[17];  
  7. int m,n;  
  8.   
  9. /*remove column c and all row i that A(i,c)==1*/  
  10. void re_move(int c)  
  11. {  
  12.     int i,j;  
  13.     L[R[c]]=L[c];                      //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.             S[C[j]]--;                 //decrease the count of column C[j]  
  21.         }  
  22. }  
  23.   
  24. /*backtrack, resume*/  
  25. void resume(int c)  
  26. {  
  27.     int i,j;  
  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 depth)  
  40. {  
  41.     int i,j,c,min=20;  
  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.         O[depth]=i;                     //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 1;  
  59.   
  60.         for(j=L[i];j!=i;j=L[j])         //backtrack  
  61.             resume(C[j]);  
  62.     }  
  63.   
  64.     resume(c);  
  65.     return 0;  
  66. }  
  67.   
  68. void init()  
  69. {  
  70.     int i,j,temp,count;  
  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.     memset(H,-1,sizeof(H));  
  81.     memset(S,0,sizeof(S));  
  82.     count=n+1;  
  83.   
  84.     for(i=1;i<=m;i++)  
  85.         for(j=1;j<=n;j++)  
  86.         {  
  87.             scanf("%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.                 L[count]=L[H[i]];    R[count]=H[i];   //连接同一行的左右节点  
  95.                 R[L[H[i]]]=count;    L[H[i]]=count;  
  96.             }  
  97.   
  98.             U[count]=U[j];   D[count]=j;              //连接同一列的上下节点  
  99.             D[U[j]]=count;   U[j]=count;  
  100.             C[count]=j;                               //该节点属于列j  
  101.             S[j]++;                       
  102.             count++;  
  103.         }  
  104. }  
  105.   
  106. int main()  
  107. {  
  108.     while(scanf("%d%d",&m,&n)!=EOF)  
  109.     {  
  110.         init();  
  111.         if(dfs(0))  
  112.             printf("Yes, I found it\n");  
  113.         else  
  114.             printf("It is impossible\n");  
  115.     }  
  116.     return 0;  


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值