DLX算法一览

目录:
1 X思想的了解。
2. 链表的递归与回溯。
3. 具体操作。
4. 优化。
5. 一些应用与应用中的再次优化(例题)。
6. 练手题

X思想的了解。

首先了解DLX是什么?

DLX是一种多元未饱和型指令集结构,DLX 代表中级车、加长轴距版本、内饰改款、尊贵车豪华版车型。—百科百度

不不不,我不讲这些明明就是不懂

DLX是什么,一种解决精准覆盖问题的做法,一般不叫算法,下面讲。

模版题:
时间限制:10000ms
单点时限:1000ms
内存限制:256MB
描述

小Ho最近遇到一个难题,他需要破解一个棋局。

棋局分成了n行,m列,每行有若干个棋子。小Ho需要从中选择若干行使得每一列有且恰好只有一个棋子。

比如下面这样局面:

在这里插入图片描述

其中1表示放置有棋子的格子,0表示没有放置棋子。

在这里插入图片描述

对于上面这个问题,小Ho经过多次尝试以后得到了解为选择2、3、4行就可以做到。

但是小Ho觉得自己的方法不是太好,于是他求助于小Hi。

小Hi:小Ho你是怎么做的呢?

小Ho:我想每一行都只有两种状态,选中和未被选中。那么我将选中视为1,未选中视为0。则每一种组合恰好对应了一个4位的01串,也就是一个4位的二进制数。

小Hi:恩,没错。

小Ho:然后我所做的就是去枚举每一个二进制数然后再来判定是否满足条件。

小Hi:小Ho你这个做法本身没什么问题,但是对于棋盘行数再多一点的情况就不行了。

小Ho:恩,我也这么觉得,那你有什么好方法么?

小Hi:我当然有了,你听我慢慢道来。

提示:跳舞链

输入

第1行:1个正整数t,表示数据组数,1≤t≤10。

接下来t组数据,每组的格式为:

第1行:2个正整数n,m,表示输入数据的行数和列数。2≤n,m≤100。

第2…n+1行:每行m个数,只会出现0或1。

输出

第1…t行:第i行表示第i组数据是否存在解,若存在输出"Yes",否则输出"No"。

样例输入
2
4 4
1 1 0 1
0 1 1 0
1 0 0 0
0 1 0 1
4 4
1 0 1 0
0 1 0 0
1 0 0 0
0 0 1 1
样例输出
No
Yes

传送门

DL=Dancing Link跳舞链(双向十字链表),一种数据结构,用来优化X算法的,所以叫DLX,所以在严格意义上来讲,DLX就是一个优美的暴力可人家就是快,就是牛逼呀!

链表大家都知道,如果这都不知道,这篇文章你多半看不懂的!

双向十字链表是什么?

一个图祝大家秒记:

在这里插入图片描述

没错,双向十字链表有四个方向的链,而普通的双向链表只有两个方向的链。所以他更牛逼

那么删除就更原来一样呀!

那么,X思想是什么。我是不是想Y了。。。

对于一个矩阵:

在这里插入图片描述

对于这种图,我们先找到第一个没有覆盖的列:
在这里插入图片描述

然后依次找一个这一列为1的行,然后将这一列与这一行标为紫色。

在这里插入图片描述

霸王硬上弓,删掉!

不对,删掉这一行还会有一行被覆盖。

在这里插入图片描述

Look,这一行的橙色部分也被覆盖,因此橙色这一列也应该被删掉。

于是,我们应当把第三行删掉(蓝色部分)。

在这里插入图片描述

这样把所有被颜色圈住的格子删掉,同时将第一行丢入ans数组。

在这里插入图片描述

丑得一批。。。

那么,照旧,选择第一个没有被覆盖的列,第二列,同时我们选择第二行作为ans。

在这里插入图片描述

删除之后,我们继续找,发现第三列还没被覆盖,但是这一列没有一行有1了(完全连行都没有了。。。失败?)

不存在的,回溯大法好呀!
在这里插入图片描述

用填色的部分就是删除的部分(先将第一列删除,再将覆盖第一列的第一行与第二行删除,同时,我们选择了第二行,所以我们将第2、5行删除),同时我们将ans[1]=2。

这么一删,我们把第1、2、5列给删了,同时第1、2行也被删了,重复以下步骤,我们发现只要选择2、3行,就木有问题了。

但是,回溯过程代价打得一批这不是你画图丑得一批的理由!!!

链表的递归与回溯。

我们发现用链表不仅删除十分快,而且回溯也十分迅猛,在空间与时间上都十分优秀!

我们只需要用链表储存1的位置,同时,把每一列的编号也作为一个节点就可以了,然后用双向十字链表建个图,然后进行那些步骤(每次跳0号节点的右边,选列的话只需要跳选的节点的下面或上面,双向链表是循环的)。

在这里插入图片描述

图中第一行中0、1、2、3、4、5代表列数,而下面的1代表这个节点的权值是1。

其实链表还有个重要的性质:
平常链表删除只是让左边的指向自己右边,同时右边又指向自己左边,但是自己的左边和右边还是指向他们的,如果要恢复的话,只需要让左边和右边的人再次指向自己就好了,真是方便。

不过双向十字链表要注意上下左右都要删除与他的联系。

所以为什么叫跳舞链,我怎么知道?

具体实现

定义代码:

#include<cstdio>
#include<cstring>
using  namespace  std;
int  a[2100];//添加时记录第i个1所在的列数 
struct  node
{
   
    int  l,r,u,d,lie,hang;//l代表左,r代表右,u代表上,d代表下,lie代表lie标记,为优化做准备,而hang则是hang坐标,经常能有许多有用的信息。 
};
struct  DLX
{
   
    node  p[610000];int  len;//p代表链表,len代表节点数 
    int  size[2100],last[2100];//size代表第i列有多少节点,而last数组记录第i列的最后一个节点编号 
    inline  void  make(int  l,int  r,int  u,int  d,int  lie,int  hang){
   p[len].l=l;p[len].r=r;p[len].u=u;p[len].d=d;p[len].lie=lie;p[len].hang=hang;}//制造函数
    inline  void  sxdel(int  x){
   p[p[x].u].d=p[x].d;p[p[x].d].u=p[x].u;}//上下链表删除
    inline  void  nsxdel(int  x){
   p[p[x].u].d=p[p[x].d].u=x;}//上下链表还原 
    inline  void  zydel(int  x){
   p[p[x].l].r=p[x].r;p[p[x].r].l=p[x].l;}//左右链表删除
    inline  void  nzydel(int  x){
   p[p[x].l].r=p[p[x].r].l=x;}//左右链表还原 
}dlx;

初始化:

inline  void  clear(int  x)//初始化x列
{
   
	len=0;p[0].l=p[0].r=p[0].u=p[0].d=0;//将0号节点初始化 
	for(int  i=1;i<=x;i++)//建x列 
	{
   
		size[i]=0;last[i]=i;//重置size与last 
		len++;make(i-1,p[i-1].r,i,i,i,0);//make制造第i列节点 
		nzydel(i);//将左右的人指向自己 
	}
}

将第row行插入到链表里,插入的节点数为a[0]。

inline  void  add(int  row)//添加第row行
{
   
	if(a[0]==0)return  ;//其实不加也可以,即使下面make了一个没用的节点,但是并不会访问到它
	len++;make(len,len,last[a[1]],p[last[a[1]]].d,a[1],row);
	nsxdel(len);size[a[1]]++;last[a[1]]=len;//制造本行第一个节点 
	for(int  i=2;i<=a[0];i++)//遍历 
	{
   
		len++;make(len-1,p[len-1].r,last[a[i]],p[last[a[i]]].d,a[i],row);//制造第i个节点 
		nsxdel(len);nzydel(len);size[a[i]]++;last[a[i]]=len;//让上下左右的节点指向自己。 
	}
}

删除(递归中的删除):

//将第i列删除,同时将相关的行也彻底删除 
inline  void  del(int  x)//注意:x的行数为0
{
   
	zydel(x);//先删掉第x列与其他列的练习 
	for(int  i=p[x].u;i!=x;i=p[i].u)//找到这一列为1的行 
	{
   
		for(int  j=p[i].l;j!=i;j=p[j].l)sxdel(j),size[p[j].lie]--;//将这一行删掉。 
	}
}

回溯:

//这就不多讲了,不过就反过来罢了 
inline  void  back(int  x)//x的行数为0
{
   
	nzydel(x);
	for(int  i=p[x].u;i!=x;i=p[i].u)
	{
   
		for(int  j=p[i].l;j!=i;j=p[j].l)nsxdel(j),size[p[j].lie]++;
	}
}

优化

还没讲重点吧!

我们发现,输出答案的话,与一开始选择的列数并没有多大关系(原本是直接选择p[0].r),所以我们可以选择列数中节点最少的作为对象,减少递归次数!

//递归过程 
int  dance(int  x)
{
   
	if(!p[0].r)return  x;//结束,返回 
	int  first,mi=999999999;
	for(int  i=p[0].r;i;i=p[i].r)//找最少列 
	{
   
		if(size[p[i].lie]<mi)mi=size[p[i].lie],first=i;
	}
	if(mi==0)return  0;//有一列没有办法覆盖?返回0 
	del(first);//先删除 
	for(int  i=p[first].u;i!=first;i=p[i].u)
	{
   
		for(int  j=p[i].l;j!=i;j=p[j].l)del(p[j].lie);//将这一行能覆盖的区域删除 
		int  tt=dance(x+1);//递归 
		if(tt)return  tt;
		for(int  j=p[i].l;j!=i;j=p[j].l)back(p[j].lie);//回溯 
	}
	back(first);
	return  0;
}

这样就完了?

其实这样还很慢!

注意这里:

for(int  j=p[i].l;j!=i;j=p[j].l)del(p[j].lie);//将这一行能覆盖的区域删除 
int  tt=dance(x+1);//递归 
if(tt)return  tt;
for(int  j=p[i].l;j!=i;j=p[j].l)back(p[j].lie);//回溯 

我们发现:
原本在删除的时候,第一个列与第二个列中相交的行数在第一次删除就没了,但是在回溯的时候,第一个列与第二个列中相交的行数在第一次回溯又回来了,而第二列中又多遍历了一遍,后面也是如此!

但是,这么打就没问题了:

for(int  j=p[i].l;j!=i;j=p[j].l)del(p[j].lie);//将这一行能覆盖的区域删除 
int  tt=dance(x+1);//递归 
if(tt)return  tt;
for(int  j=p[i].r;j!=i;j=p[j].r)back(p[j].lie);//回溯 

完整代码:

#include<cstdio>
#include<cstring>
using  namespace  std;
int  a[2100];//添加时记录第i个1所在的列数 
struct  node
{
   
    int  l,r,u,d,lie,hang;//l代表左,r代表右,u代表上,d代表下,lie代表lie标记,为优化做准备,而hang则是hang坐标,经常能有许多有用的信息。 
};
struct  DLX
{
   
    node  p[610000];int  len;//p代表链表,len代表节点数 
    int  size[2100],last[2100];//size代表第i列有多少节点,而last数组记录第i列的最后一个节点编号 
    inline  void  make(int  l,int  r,int  u,int  d,int  lie,int  hang){
   p[len].l=l;p[len].r=r;p[len].u=u;p[len].d=d;p[len].lie=lie;p[len].hang=hang;}//制造函数 
    inline  void  sxdel(int  x){
   p[p[x].u].d=p[x].d;p[p[x].d].u=p[x].u;}//上下链表删除
    inline  void  nsxdel(int  x){
   p[p[x].u].d=p[p[x].d].u=x;}//上下链表还原
    inline  void  zydel(int  x){
   p[p[x].l].r=p[x].r;p[p[x].r].l=p[x].l;}//左右链表删除
    inline  void  nzydel(int  x){
   p[p[x].l].r=p[p[x].r].l=x;}//左右链表还原 
    inline  void  clear(int  x)//初始化x列
	{
   
		len=0;p[0].l=p[0].r=p[0].u=p[0].d=0;//将0号节点初始化 
		for(int  i=1;i<=x;i++)//建x列 
		{
   
			size[i]=0;last[i]=i;//重置size与last 
			len++;make(i-1,p[i-1].r,i,i,i,0);//make制造第i列节点 
			nzydel(i);//将左右的人指向自己 
		}
	}
    inline  void  add(int  row)//添加第row行
    {
   
    	if(a[0]==0)return  ;//其实不加也可以,即使下面make了一个没用的节点,但是并不会访问到它
    	len++;make(len,len,last[a[1]],p[last[a[1]]].d,a[1],row);
    	nsxdel(len);size[a[1]]++;last[a[1]]=len;//制造本行第一个节点 
    	for(int  i=2;i<=a[0];i++)//遍历 
    	{
   
    		len++;make(len-1,p[len-1].r,last[a[i]],p[last[a[i]]]
  • 17
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值