目录:
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]]]