Dancing Links X专题学习(代码版)

Dancing Links X专题学习

写在前面:本人萌新,这篇博文是建立在众位大佬的博客之上,然后在自己学习的过程中加入自我分析。欢迎吐槽!

关键词:递归 | 回溯 | 深度优先 | 非确定性的
应用:精确覆盖 | 数独 | 重复覆盖 | 等

一 . 精确覆盖

【引例】Girl Match
在一个寒冷的冬天,卖女孩的小火柴 yy 来到了一家奇特的餐馆 ——Match Girl。该餐馆中有 N 种食品(食品按 1~N 编号)。在这个奇怪的餐馆里,食品都按套餐出售。已知有 M 种套餐,对于每一种套餐,给出该套餐包含的食品编号(保证不存在两个完全相同的套餐)。yy 饿极了,她希望能够品尝到每一种食品。但 yy 由于没有能够凭借美色拐到足够的女孩,现在穷得响叮当。她希望能够购买到所有的食品且不重复购买同一种食品。她已经饿得没有力气继续思考了,而你是她拐回来的唯一的“女孩”。作为回报,她不会把你卖了。她能够实现这小小的愿望吗?如果能,输出"Yes";否则,输出"No"。

现有 6 种食品,5 种套餐:
套餐 1 :1 3
套餐 2 :1 3 4 6
套餐 3 :2 5
套餐 4 :3 4 5 6
套餐 5 :1 2 3 4 5
答案为:Yes(选择套餐2和套餐3)

穷举、状压等方法都可以完成 N、M 较小的部分数据,这里不再一一赘述。直接引入 Dancing Links X 的思考过程。

将套餐和对应的食品编号转化成0/1矩阵,如下图,第 i 行代表第 i 个套餐对应的食品编号(1为包含该编号的食品)。我们要解决的问题就成了选取一个行集合,使得选出集合后所得到矩形的每一列上有且仅有一个1。

1 0 1 0 0 0 —— 第一行
1 0 1 1 0 1 —— 第二行
0 1 0 0 1 0 —— 第三行
0 0 1 1 1 1 —— 第四行
1 1 1 1 1 0 —— 第五行

这就是经典的精确覆盖问题。更常见的问题,需要同时保证所选的行数最少,并输出方案。

再回到这个问题。

对于该0/1矩阵,首先假定我们选择了第一行。那么第一行以及与第一行有冲突的行都没有可能在后面选择到(*有"冲突"表示的是:该行与其它行存在共同的都为 1 的列)。所以,我们可以将与第一行有冲突的行都删除掉。同时,在以后选择的行中不再可能存在与第一行所有共同为 1 的列,将第一行对应为 1 的列也对应删除掉,如 图1-1 所示:

enter image description here

同样,再进行删除,将得到一个空矩阵。由此,我们可以发现:我们在原矩阵中选择了 2 行,但只选择了 4列(共 6 列)。所以,该选择方案不成立,那么不断回溯,重新进行选择。由于最后的选择只有一种,那么直接回溯到第一次的选择。

选择删除第二行,如图 1-2 :

enter image description here

再进行删除。此次操作,我们在原矩阵中选择了 2 行,对应选择了 6 列(共 6 列)。得到答案。

在这个搜索过程中,我们调用了大量矩阵缓存和矩阵回溯。如果用传统的矩阵存储,每次的删除,我们最坏的时间复杂度为O( NM )。如果该方案搜索失败,又需要用O( NM )的复杂度回溯。这种操作的时间复杂度难以解决。

于是,由算法大师Donald Knuth提出了"X算法"。这是一种递归的、非确定的、深度优先的回溯算法。Dancing Links,即舞蹈链,本身是一种链式的数据结构。利用链表的性质,不再在删除操作中开辟更多的空间,以O(1)的时间实现删除列,以<=O(N)的时间删除行。也是这样,Dancing Links由此得名。而"Dancing Links X"的含义正是利用"舞蹈链"来求解"X算法"的意思。(*本段转载非原创,详见 英雄哪里出来 文章第三部分Dancing Links X算法 第 4 点Dancing Links

假设我们现在 A 以链表的形式存储,在链表的更改操作如下:

A[A[i].Left].Right->A[i].Left
A[A[i].Right].Left->A[i].Right

我们可以发现:A[i].Left 和 A[i].Right 的指向都发生了变化,但是 A[i] 本身没有,如果我们想要将 A[i] 重新放入,只需要更改为

A[A[i].Left].Right->i
A[A[i].Right].Left->i

即可。

Dancing Links其实是一种十字交叉双向循环链表。依靠链表的这一性质,快速地将某一列或某一行先移除,然后在更改回来。Dancing Links的节点可以分为以下四类。

  1. 总表头:连接行首与列首节点。
  2. 列首节点
  3. 行首节点:可以看做是一个指针数组,Row[i]记录了第 i 行第一个节点的编号。
  4. 元素节点

如 图1-3 所示:

enter image description here

(* 注明:行首节点 Row 由于是单向不再在图中画出)

算法的流程如下(译自维基百科):

  1. 判断矩阵是否为空(行) 。行为空且列为空,记录答案并终止查询;列不为空,返回上层并继续查询。否则,继续往后。

  2. 选择列 c (确定性选择)

  3. 选择一行 r,使得 A[r,c]=1 (非确定性选择)

  4. 将 r 统计进临时答案变量中

  5. 枚举每列 j 使得 A[r,j]=1;
    枚举每行 k 使得 A[k,j]=1;
    从矩阵中删除第 i 行

  6. 在简化矩阵中重复算法

用 引例 中给出的数据,模拟过程为:

移除第一行以及与它冲突的。

enter image description here

继续移除:

enter image description here

发现矩阵不为空(依旧存在列首节点),没有找到答案。

回溯到 图1-6 所示:

enter image description here

回溯到 图1-7 所示:

enter image description here

移除第二行以及与它冲突的:

enter image description here

继续移除:

enter image description here

矩阵为空,找到答案。

再做 Dancing Links 的过程中,每次寻找哪一列的 1 个数最少。然后枚举该列为 1 的行,选择删除。这样能够较大幅度加快寻找的速度。

预处理:

void Ready(){
   
   
    	for(int i=0;i<=C;i++){
   
   // 0 为 Head 
    	    Sum[i]=0;
    		Node[i].Up=i;
    		Node[i].Down=i;
    		Node[i].Left=i-1;
    		Node[i].Right=i+1;
    		Node[i].Col=i;
    		Node[i].Row=0;
    	}//第一行(列首节点)初始化 
    	Node[0].Left=C;
    	Node[C].Right=0;
    	cnt=C;
    	for(int i=1;i<=R;i++) Row[i]=0;//行首节点初始化
    }

加入元素节点:

void Push(int x,int y){
   
   
	cnt++;//新建一个点
	Sum[y]++;//统计该列节点数
	Node[cnt].Down=Node[y].Down;//将当前元素放在第 y 列的第一个 
	Node[cnt].Up=y;
	Node[Node[y].Down].Up=cnt;
	Node[y].Down=cnt;
	Node[cnt].Row=x;
	Node[cnt].Col=y; 
	if(!Row[x]){
   
   
		Row[x]=cnt;
		Node[cnt].Left=cnt;
		Node[cnt].Right=cnt;//当前行没有节点 
	}else{
   
   
		Node[cnt].Left=Row[x];
		Node[cnt].Right=Node[Row[x]].Right;
		Node[Node[Row[x]].Right].Left=cnt;
		Node[Row[x]].Right=cnt;//将新建节点放在该行的第一个节点的后面
	}
}//节点插入的写法有很多

删除操作:


    void Delete(int c){
   
   //选中第 c 列 
	Node[Node[c].Left].Right=Node[c].Right;
	Node[Node[c].Right].Left=Node[c].Left;
	for(int i=Node[c].Down;i!=c;i=Node[i].Down)
	 for(int j=Node[i].Right;j!=i;j=Node[j].Right){
   
   
	 	Node[Node[j].Up].Down=Node[j].Down;
	 	Node[Node[j].Down].Up=Node[j].Up;
	 	Sum[Node[j].Col]--;
	 }//删除该列所有 1 所在行的所有数 
  } 

回溯:


    void Return(int c){
   
   
	for(int i=Node[c].Up;i!=c;i=Node[i].Up)
	 for(int j=Node[i].Left;j!=i;j=Node[j].Left){
   
   
	 	Node[Node[j].Up].Down=j;
	 	Node[Node[j].Down].Up=j;
	 	Sum[Node[j].Col]++;
	 }
    }

舞蹈:

  bool Dancing(int Dep){
   
   
  if(!Node[0].Right){
   
   
  	N=Dep;
      return true;
  }//矩阵是否为空
  int c=Node[0].Right;
  for(int i=Node[c].Right;i;i=Node[i].Right){
   
   
  	if(Sum[i]<Sum[c])
  	 c=i;
  }//找最小
  Delete(c);
  for(int i=Node[c].Down;i!=c;i=Node[i].Down){
   
   
  	Ans[Dep]=Node[i].Row;//记录答案
  	for(int j=Node[i].Right;j!=i;j=Node[j].Right) Delete(Node[j].Col); 
  	if(Dancing(Dep+1)) return true;
  	for(int j=Node[i].Left;j!=i;j=Node[j].Left) Return(Node[j].Col);
  } 
  Return(c);
  return false;
}

*由于此题不存在,不附上完全代码

【例题1】Easy Finding

给定一个M*N(M<=16,N<=300)的0/1矩阵,寻找一些行,使得重新得到的矩阵的每一列上有且仅有一个1。如果有解,输出"Yes, I found it";否则,输出"It is impossible"。

Dancing Links X的模板题。按照描述的矩阵构图套上模板。

#include<bits/stdc++.h>
int R,C,cnt,N,a[510][1010]={
   
   },Row[510]={
   
   },Sum[1010]={
   
   };
struct Dancing_Link_X{
   
   
	int Up,Down,Left,Right;
   	int Col,Row;
} Node[500010]={
   
   };
void Ready(){
   
   
    for(int i=0;i<=C;i++){
   
   
  		Sum[i]=0;
   		Node[i].Up=i;
   		Node[i].Down=i;
   		Node[i].Left=i-1;
   		Node[i].Right=i+1;
		Node[i].Col=i;
    	Node[i].Row=0;
    }
    Node[0].Left=C;
    Node[C].Right=0;
    cnt=C;
    for(int i=1;i<=R;i++)
     Row[i]=-1;
}
void Push(int x,int y){
   
   
    Sum[y]++;
    cnt++;
    Node[cnt].Down=Node[y].Down;
    Node[cnt].Up=y;
   	Node[Node[y].Down].Up=cnt;
   	Node[y].Down=cnt;
   	Node[cnt].Row=x;
   	Node[cnt].Col=y; 
    if(Row[x]<0){
   
   
   		Row[x]=cnt;
   		Node[cnt].Left=cnt;
   		Node[cnt].Right=cnt;
   	}else{
   
   
   		Node[cnt].Left=Row[x];
    	Node[cnt].Right=Node[Row[x]].Right;
    	Node[Node[Row[x]].Right].Left=cnt;
    	Node[Row[x]].Right=cnt;
    }
}
void Delete(int c){
   
   
    Node[Node[c].Left].Right=Node[c].Right;
   	Node[Node[c].Right].Left=Node[c].Left;
   	for(int i=Node[c].Down;i!=c;i=Node[i].Down)
     for(int j=Node[i].Right;j!=i;j=Node[j].Right){
   
   
   	 	Node[Node[j].Up].Down=Node[j].Down;
   	 	Node[Node[j].Down].Up=Node[j].Up;
   	 	Sum[Node[j].Col]--;
       	 }
	} 
void Return(int c){
   
   
    for(int i=Node[c].Up;i!=c;i=Node[i].Up)
     for(int j=Node[i].Left;j!=i;j=Node[j].Left){
   
   
       	Node[Node[j].Up].Down=j;
      	Node[Node[j].Down].Up=j;
       	Sum[Node[j].Col]++;
    }
    Node[Node[c].Left].Right=c;
    Node[Node[c].Right].Left=c;
}     
bool Dancing(int Dep){
   
   
   if(!Node[0].Right) return true;
   int c=Node[0].Right;
    for(int i=Node[c].Right;i;i=Node[i].Right){
   
   
            if(Sum[i]<Sum[c])
             c=i;
    }
    Delete(c);
    for(int i=Node[c].Down;i!=c;i=Node[i].Down){
   
   
        for(int j=Node[i].Right;j!=i;j=Node[j].Right) Delete(Node[j].Col); 
        if(Dancing(Dep+1)) return true;
        for(int j=Node[i].Left;j!=i;j=Node[j].Left) Return(Node[j].Col);
        } 
        Return(c);
        return false;
    }
int main(){
   
   
    for(;scanf("%d%d",&R,&C)!=EOF;){
   
   
    	Ready();
    	for(int i=1;i<=R;i++)
    	 for(int j=1;j<=C;j++){
   
   
    	 	scanf("%d",&a[i][j]);
    	 	if(!a[i][j]) continue;
    		Push(i,j); 
    	}
    	if(Dancing(0))printf("Yes, I found it\n");
    	else printf("It is impossible\n");
    }
    return 0;
}

【例题2】Treasure Map
你的老板有许多同张藏宝图的副本。不幸的是,这些藏宝图的副本被打破成若干矩形的碎片。更糟糕的是,有些碎片丢失了。还好,可以得知每个碎片在藏宝图中的位置(给出一个范围,用x1,y1,x2,y2表示,x1<x2,y1<y2)现在老板要求你用这些碎片组成一张完整的藏宝图。(不必使用所有的碎片,但不允许重叠)。藏宝图的大小为N*M(N,M<=30),碎片数不超过300.(如图)

enter image description here

将N*M个格子的覆盖情况看作Dancing Links的列,每个小碎片看作行。将每个碎片所能覆盖的格子标为 1 ,作为元素节点加入。
细节的处理上,由于藏宝图的左下角由(0,0)开始,为了方便处理,将所有碎片的 x1、y1 都加上1。由于题目保证 x1<x2 且 y1<y2,碎片的相对位置没有改变,不会对答案造成影响。这张图就变为左下角由(1,1)开始的了。

#include<bits/stdc++.h>
using namespace std;
const int R=30*30,C=500;
int Ans,N,M,P,cnt,sum[R+10]={
   
   },Row[C+10]={
   
   };
struct Node{
   
   
	int L,R,U,D;
	int Row,Col;
} pnt[R*C+10]={
   
   };
void Ready(int r,int c){
   
   
	for(int i=0;i<=c;i++){
   
   
		pnt[i].U=pnt[i].D=i;
		pnt[i].L=i-1;
		pnt[i].R=i+1;
		pnt[i].Col=i;
		sum[i]=0;
	}
	pnt[0].L=c;
	pnt[c].R=0;
	cnt=c;
	for(int i=1;i<=r;i++) Row[i]=0;
}
inline void Add(int x,int y){
   
   
	cnt++; 
	sum[y]++;
	pnt[cnt].Row=x;
	pnt[cnt].Col=y;
	pnt[cnt].U=y;
	pnt[cnt].D=pnt[y].D;
	pnt[pnt[y].D].U=cnt;
	pnt[y].D=cnt;
	if(!Row[x]){
   
   
		Row[x]=cnt;
		pnt[cnt].L=pnt[cnt].R=cnt;
	}else{
   
   
		pnt[cnt].R=Row[x];
		pnt[cnt].L=pnt[Row[x]].L;
		pnt[pnt[Row[x]].L].R=cnt;
		pnt[Row[x]].L=cnt;
		Row[x]=cnt;
	}
}
inline void del(int c){
   
   
	pnt[pnt[c].L].R=pnt[c].R;
	pnt[pnt[c].R].L=pnt[c].L;
	for(int i=pnt[c].D;i!=c;i=pnt[i].D){
   
   
		for(int j=pnt[i].R;j!=i;j=pnt[j].R){
   
   
			pnt[pnt[j].U].D=pnt[j].D;
			pnt[pnt[j].D].U=pnt[j].U;
			sum[pnt[j].Col]--;
		}
	}
}
inline void ret(int c){
   
   
	for(int i=pnt[c].U;i!=c;i=pnt[i].U){
   
   
		for(int j=pnt[i].L;j!=i;j=pnt[j].L){
   
   
			pnt[pnt[j].U].D=pnt[pnt[j].D].U=j;
			sum[pnt[j].Col]++;
		}
	}
	pnt[pnt[c].L].R=pnt[pnt[c].R].L=c;
}
inline void Dancing(int Dep){
   
   
	if(Dep>=Ans&&Ans!=-1) return ;
	if(!pnt[0].R){
   
   
		if(Ans!=-1) Ans=min(Dep,Ans);
		else Ans=Dep;
		return ;
	}
	int c=pnt[0].R;
	for(int i=pnt[0].R;i;i=pnt[i].R){
   
   
		if(sum[i]<sum[c])
		 c=i;
	}
	del(c);
	for(int i=pnt[c].D;i!=c;i=pnt[i].D){
   
   
		for(int j=pnt[i].R;j!=i;j=pnt[j].R) del(pnt[j].Col);
		Dancing(Dep+1);
		for(int j=pnt[i].L;j!=i;j=pnt[j].L) ret(pnt[j].Col);
	}
	ret(c);
}
int main(){
   
   
	int T; scanf("%d",&T);
	for(;T--;){
   
   
		scanf("%d%d%d",&N,&M,&P);
		Ready(P,N*M);
		Ans=-1;
		for(int i=1;i<=P;i++){
   
   
			int x,y,X,Y;
			scanf("%d%d%d%d",&x,&y,&X,&Y);
			for(int j=x+1;j<=X;j++)
			 for(int k=y+1;k<=Y;k++)
			  Add(i,(j-1)*M+k);
		}
		Dancing(0);
		printf("%d\n",Ans);
	}
	return 0;
}

【例题3】Dominoes
需要用12种方块覆盖一个NM的棋盘,每个方块只能使用一次。求有多少种覆盖方案(方案翻转或旋转后相同算同一种方案。方块可以旋转,可以翻转)。N*M=60
enter image description here

由图可知,每个方块的大小为5,60的棋盘覆盖必定每一个方块都必须用上。且如果min(N,M) < 3,那么答案必定会为0。所以,60个格子的覆盖情况和12种方块的使用情况看作列,所有单个方块可能置的情况看作行。方块旋转和翻转后,不同的情况一共有63种,63种情况又可能放在棋盘中不同的位置(<63 * 60个),由这么多种情况看作行。直接做的话会超时。由于N*M=60,而60的因子十分少。我们可以用Dancing Links先处理处每一种答案,然后打表。
在细节的处理上,需要注意在选择列时,只需要选择前60列进行移除。12种方块的使用情况相当于一个约束条件:在方块放置时,该种方块的其他情况会被删除,相当于只选择了一次该方块。

#include<bits/stdc++.h>
using namespace std;
const int R=60*63,C=60+12;
const int mp[63][5][5]={
   
   
    {
   
   
        {
   
   1,0,0},
        {
   
   1,0,0},
        {
   
   1,1,1},
    },{
   
    
        {
   
   1,1,1},
        {
   
   1,0,0},
        {
   
   1,0,0},
    },{
   
   
        {
   
   1,1,1},
        {
   
   0,0,1},
        {
   
   0,0,1},
    },{
   
   
        {
   
   0,0,1},
        {
   
   0,
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值