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的模板题。按照描述的矩阵构图套上模板。

【例题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)开始的了。

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

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

【例题4】 NQUEEN - Yet Another N-Queen Problem
  在解决N皇后的问题后,LoadingTime想要解决一个更难版本的N皇后问题。在这个问题中,一些皇后已经被放在了特定的位置。希望你将其他皇后放在棋盘上,使得每两个皇后不互相攻击(同样地,皇后能攻击该行、该列即两条对角线上的所有位置)。你能帮助他解决这个问题吗?要求输出第i行上皇后的列号(保证有解)。N<=50,时限:0.640s

  将皇后的N行的覆盖情况,N列的覆盖情况,2N-1 条主对角线和 2N-1 条副对角线的覆盖情况都作为Dancing Links列(共计6N-2列),将皇后在棋盘的情况看作行(最多N*N种情况)。
  细节的处理上,需要注意变量的清空(多组数据);需要注意对角线不一定要都覆盖,在选择列时,只需要选择前2N列,4N-2条对角线相当于一个约束条件。在棋子放置时,与其在同一条对角线的情况会被删除。

二.数独系列
  即可以看做特殊系列的精确覆盖问题。
  以一个9*9的数独举例:

  1. 第1~81列代表81个格子的覆盖情况。
  2. 第82~2*81列代表了9行 1 ~ 9数字分防置情况
  3. 第281+1~381列代表了9列 1 ~9数字的防置情况
  4. 381+1~481列代表9个"宫" 1 ~ 9数字的防置情况

  81*9行,代表81个位置防置1~9的情况。

  其中,宫号的计算方式为(从0开始):宫号=(行/3)*3 + (列/3)。

【例题5】Sudoku
  9*9数独。

  模板,按照如上即可建图跑精确覆盖即可。

【例题6】靶形数独
  9*9数独形式填数。离中心的距离不同,格子有同的价值,如 图2-1 所示。而某一格子最终的价值为该格子填入的数 * 格子的价值。求最大价值。

enter image description here

  对于这道题,在9*9的数独基础上,需要将所有的情况都跑一遍,将答案不断更新。

【例题7】Sudoku
  16*16数独

  较9*9来,只需要将范围改成16 * 16即可。

三.重复覆盖
【引例】买点彩票压压惊
(*本题以及分析来自 英雄哪里出来 原文:https://blog.csdn.net/whereisherofrom/article/details/79220897
  有这样一种彩票,规则如下:总共八个数字,范围是[1,8]。八选五,如果五个都中,则为特等奖;如果中了其中四个,则为一等奖。作者觉得连年会都抽不到奖的人来说特等奖的概率太小了,所以对特等奖基本不抱希望。但是想尝试下一等奖,于是他想知道至少要买多少张彩票才能使得他中一等奖的概率为100%。

  如果这个问题问的是让特等奖概率为100%,会变得简单许多。可以这么考虑,C(8,5)种情况下,每种情况都有可能中奖,所以每张彩票都必须买才能保证没有漏网之鱼。所以只要买下C(8,5)=56张彩票,就能保证一定有一张能中特等奖。

  但是,如果五个里面中四个,情况就不一样了。假设我买了两张彩票,一张为{1,2,3,4,5},一张为{4,5,6,7,8},但是中一等奖的四个数字为{1,2,3,6}。虽然两张彩票覆盖了所有数字,但是第一张只中了三个数字{1,2,3};第二张之中了一个数字{6},因而都不算中奖。

   为了应对任何一种中奖情况,我们需要做这样一件事情:从所有的C(8,5)种组合,也就是彩票中挑选出K种彩票,使得这K种彩票里能够找到任意的C(8,4)的组合({1,2,3,4},{1,2,3,5},…{4,5,6,7}…等等),并且使得这个K最小。为了使问题更加通俗易懂,我们减小数据量,考虑“四选三中二”的情况(四个数字选三个,中其中二个才算中奖)。

  这是一个矩阵,矩阵的行代表所有的彩票组合(四选三),矩阵的列代表所有中奖组合(四中二)。对于每一行,如果这种组合包含对应的中奖组合,那么将它对应矩阵的位置图上颜色。为了看起来不混淆,第一种组合方案采用红色,第二种橙色,以此类推…
enter image description here
  然后我们把这个矩阵数字化,有颜色的地方置为1,没有颜色的置为0,得到了如下矩阵:
enter image description here

   这个矩阵有一个特点就是:“每行三个1”,这是肯定的,因为对于每一行来说,要从三个数字中挑出所有中了两个数字的情况,即C(3,2)=3。但是这不是我们关心的重点,我们关心的是如何选择一些行集合,使得所有列都能被选到(或者说覆盖到)。

   问题即变成:是否存在这样一个行集合,使得集合中每一列至少一个"1"?

   (转载至此)

   重复覆盖的移除过程中,假定选择了第一行。那么会将第一行与之对应位上为1的列都删除掉,而不再删除与它冲突的列。这样的复杂度不再如精确覆盖般优秀,但是答案的正确性得到了保证。 以如上样例作为模板,过程如下:

enter image description here

  选择第一行,删除:

enter image description here

  由图可知,与第一行有冲突的行并没有删除。继续操作直到矩阵只剩下总表头(完全为空)即可。

  本题即用重复覆盖+IDA*。

【例题8】SquareDestroyer
  N*N用火柴组成的正方形,已经删除若干个木棍(给出木棍编号,原木棍编号如图)。求至少要删除多少木棍使得原矩阵不存在一个完整的正方形?(N<=5)
enter image description here

  将每个正方形作为列,将木棍作为行,IDA*完成重复覆盖。

四.小结

  • 要注意变量的清空
  • 要注意数组越界会发生奇奇怪怪的错误
  • 不要觉得困难,多抄抄标程有助于理解
  • 其实转换没有想象中的那么难

五.参考博文

作者:英雄哪里出来;
来源:CSDN
原文:https://blog.csdn.net/whereisherofrom/article/details/79220897

六.题目拓展
(*转载自 英雄哪里出来 博文)
精确覆盖

EasyFinding ★★☆☆☆ 赤裸精确覆盖

TreasureMap ★★☆☆☆ 经典精确覆盖

Dominoes ★★★☆☆ 经典骨牌覆盖

APuzzlingProblem ★★☆☆☆ 经典骨牌覆盖

NQUEEN ★★★☆☆ N皇后问题

Lamp ★★★☆☆ 精确覆盖

PowerStations ★★★☆☆ 精确覆盖

数独系列

SudokuKiller ★☆☆☆☆ 基础题

Su-Su-Sudoku ★☆☆☆☆ 基础题

Sudoku ★☆☆☆☆ 基础题

Sudoku ★☆☆☆☆ 基础题

Sudoku ★★☆☆☆ 基础题

Sudoku ★★☆☆☆ 基础题

Sudoku ★★☆☆☆ 基础题

Sudoku ★★★☆☆ 有意思的题

Sudoku ★★★☆☆ 最全的数独题

SquigglySudoku ★★★☆☆ 数独+连通分量

重复覆盖

神龙的难题 ★★☆☆☆ 基础重复覆盖

whosyourdaddy ★★★☆☆ 重复覆盖A*基础

Radar ★★★☆☆ 二分+重复覆盖A*

Bomberman ★★★☆☆ 重复覆盖A*

RepairDepots ★★★★☆ 三角形外心+重复覆盖A*

Firestation ★★★☆☆ 重复覆盖A*

StreetFighter ★★★★☆ 精确覆盖+重复覆盖

SquareDestroyer ★★☆☆☆ 古董题

Airport ★★☆☆☆ 基础重复覆盖A*

ASimpleMathProblem ★★☆☆☆ 重复覆盖+打表

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值