预备知识:
跳跃的舞者,舞蹈链(Dancing Links)算法——求解精确覆盖问题
夜深人静写算法(九)- Dancing Links X(跳舞链)
题目链接:
https://www.luogu.org/problem/P4929
题目背景
本题是舞蹈链模板——精确覆盖问题
题目描述
给定一个N行M列的矩阵,矩阵中每个元素要么是1,要么是0
你需要在矩阵中挑选出若干行,使得对于矩阵的每一列j,在你挑选的这些行中,有且仅有一行的第j个元素为1
输入格式
第一行两个数N,M
接下来N行,每行M个数字0或1,表示这个矩阵
输出格式
一行输出若干个数表示答案,两个数之间用空格隔开,输出任一可行方案均可,顺序随意
若无解,输出“No Solution!”(不包含引号)
输入输出样例
输入 #1
3 3
0 0 1
1 0 0
0 1 0
输出 #1
2 1 3
输入 #2
3 3
1 0 1
1 1 0
0 1 1
输出 #2
No Solution!
说明/提示
N,M≤500
保证矩阵中1的数量不超过5000个
建图方式:
结点类型以及所需变量:
typedef struct DLXnode
{
int x,y;
DLXnode *up,*down,*left,*right;
}D;
D node[MAXN*MAXN];
D row[MAXN];
D col[MAXN];
D *head=new D;
int cnt,n,m,num[MAXN],ans[MAXN];
x和y表示每个结点的横、纵坐标,其中,总表头head的坐标为(0,0),列首结点的横坐标为0,行首结点的纵坐标为1,元素结点的横、纵坐标对应于给定矩阵中1的横、纵坐标;
up、down、left和right表示指向四个方向的指针;
node数组存储的是元素结点;
row数组和col数组存储的是行首结点和列首结点;
head指针指向的是总表头;
cnt表示元素结点的个数,n代表给定矩阵的行,m代表给定矩阵的列;
num数组存储的是每一列'1'的个数,即num[i]表示第i列'1'的个数;
ans数组存储的是解。
初始化图init:
void init(int n,int m)
{
cnt=0,head->x=0,head->y=0;
head->down=head->left=head->right=head->up=head;
for(int i=1;i<=m;++i)
{
col[i].x=0,col[i].y=i;
col[i].right=head,col[i].left=head->left;
col[i].right->left=col[i].left->right=&col[i];
col[i].up=col[i].down=&col[i];
num[i]=0;//第i列1的个数,初始化为0
}
for(int i=1;i<=n;++i)
{
row[i].x=i,row[i].y=0;
row[i].down=head,row[i].up=head->up;
row[i].up->down=row[i].down->up=&row[i];
row[i].left=row[i].right=&row[i];
}
}
如图:
添加元素结点add:
inline void add(int x,int y)
{
D *p=&node[++cnt];
p->x=x,p->y=y;
p->right=&row[x],p->left=row[x].left;
p->left->right=p->right->left=p;
p->down=&col[y],p->up=col[y].up;
p->up->down=p->down->up=p;
++num[y];
}
主函数里添加元素结点的顺序是:
n=read(),m=read();
init(n,m);
for(int i=1;i<=n;++i)
{
for(int j=1;j<=m;++j)
if(read()) add(i,j);
}
移除结点del以及恢复结点re:
inline void delLR(D *p)//将p指向的结点在水平方向移除
{
p->right->left=p->left;
p->left->right=p->right;
}
inline void delUD(D *p)//将p指向的结点在竖直方向移除
{
p->up->down=p->down;
p->down->up=p->up;
}
inline void reLR(D *p)//将p指向的结点在水平方向恢复
{
p->left->right=p->right->left=p;
}
inline void reUD(D *p)//将p指向的结点在竖直方向恢复
{
p->up->down=p->down->up=p;
}
如图:
移除列cover和相关的行以及恢复:
inline void cover(int c)//移除第c列 并且 将该列有'1'的行也移除
{
delLR(&col[c]);//将第c列的列首结点在水平方向断开
D *i,*j;
for(i=col[c].down;i!=&col[c];i=i->down) //遍历该列找有'1'的行
{
for(j=i->right;j!=i;j=j->right)//移除该行
{
--num[j->y];//第j->y列的'1'减少
delUD(j);//将该行的元素结点在竖直方向断开
}
delLR(i);//将第c列的元素结点在水平方向断开
}
}
inline void re(int c)//恢复第c列 以及 恢复该列有'1'的行
{
D *i,*j;
for(i=col[c].down;i!=&col[c];i=i->down)
{
reLR(i);
for(j=i->right;j!=i;j=j->right)
{
reUD(j);
++num[j->y];
}
}
reLR(&col[c]);
}
如图:
移除第c列时首先将列首结点在水平方向断开,然后向下(或向上)遍历将其元素结点也在水平方向断开。
这里由于还要将第c列有'1'即元素结点所在的行也进行移除,故在第c列每行的元素结点在水平方向断开之前,将该行所有结点在竖直方向断开,更新每列还剩下'1'的个数。
恢复第c列相关的操作可看做是移除操作的逆过程,逆着来一遍就行。
开始跳舞Dance:
bool dance(int k)
{
if(head->right==head)//空矩阵
{
for(int i=1;i<k;++i)//输出解
cout<<ans[i]<<' ';
return true;
}
int c=-1,tmp=INF;
D *i,*j;
for(i=head->right;i!=head;i=i->right)
{
if(num[i->y]<tmp)//选每一列1最少的列c,确定性选择
{
tmp=num[i->y];
c=i->y;
}
}
cover(c);//移除第c列和该列有1的行
for(i=col[c].down;i!=&col[c];i=i->down)//在列c中选行
{
i->left->right=i;//(1)
for(j=i->right;j!=i;j=j->right)
if(j->y) cover(j->y);//移除该行有1的列以及该列有1的行
i->left->right=i->right;//这句和(1)句是为了避免死循环
ans[k]=i->x;//将该行加入可行解列表
if(dance(k+1)) return true;//移除一些行和列后规模缩小,递归求解
//回溯
i->right->left=i;
for(j=i->left;j!=i;j=j->left)
if(j->y) re(j->y);
i->right->left=i->left;
}
re(c);
return false;//如果列c中无满足的行,那肯定GG了
}
我们重新思考一遍 X 算法
- 如果矩阵A没有列(即空矩阵),则当前记录的解为一个可行解;算法终止,成功返回;
- 否则选择矩阵A中“1”的个数最少的列c;(确定性选择)
- a:如果存在A[r][c]=1的行r,将行r放入可行解列表,进入步骤4(非确定性选择)
b:如果不存在A[r][c]=1的行r,则剩下的矩阵不可能完成精确覆盖,说明之前的选择有错(或者根本就无解),需要回溯,并且恢复此次删除的行和列,然后跳到步骤3.a- 对于所有的满足A[r][j]=1的列j,对于所有满足A[i][j]=1的行i,将行i从矩阵A中删除;将列j从矩阵A中删除;
- 在不断减少的矩阵A上递归重复调用上述算法;
上述引用来自https://www.luogu.org/blog/Setsugesuka/solution-p4929
AC代码:
#include<iostream>
#include<sstream>
#include<fstream>
#include<algorithm>
#include<cstring>
#include<iomanip>
#include<cstdlib>
#include<cctype>
#include<vector>
#include<string>
#include<cmath>
#include<ctime>
#include<stack>
#include<queue>
#include<map>
#include<set>
#define mem(a,b) memset(a,b,sizeof(a))
#define random(a,b) (rand()%(b-a+1)+a)
#define ll long long
#define ull unsigned long long
#define e 2.71828182
#define Pi acos(-1.0)
#define ls(rt) (rt<<1)
#define rs(rt) (rt<<1|1)
#define lowbit(x) (x&(-x))
using namespace std;
const int MAXN=5e2+5;
const int INF=0x3f3f3f3f;
int read()
{
int s=1,x=0;
char ch=getchar();
while(!isdigit(ch)) {if(ch=='-') s=-1;ch=getchar();}
while(isdigit(ch)) {x=10*x+ch-'0';ch=getchar();}
return x*s;
}
typedef struct DLXnode
{
int x,y;
DLXnode *up,*down,*left,*right;
}D;
D node[MAXN*MAXN];
D row[MAXN];
D col[MAXN];
D *head=new D;
int cnt,n,m,num[MAXN],ans[MAXN];
void init(int n,int m)
{
cnt=0,head->x=0,head->y=0;
head->down=head->left=head->right=head->up=head;
for(int i=1;i<=m;++i)
{
col[i].x=0,col[i].y=i;
col[i].right=head,col[i].left=head->left;
col[i].right->left=col[i].left->right=&col[i];
col[i].up=col[i].down=&col[i];
num[i]=0;//第i列1的个数,初始化为0
}
for(int i=1;i<=n;++i)
{
row[i].x=i,row[i].y=0;
row[i].down=head,row[i].up=head->up;
row[i].up->down=row[i].down->up=&row[i];
row[i].left=row[i].right=&row[i];
}
}
inline void add(int x,int y)
{
D *p=&node[++cnt];
p->x=x,p->y=y;
p->right=&row[x],p->left=row[x].left;
p->left->right=p->right->left=p;
p->down=&col[y],p->up=col[y].up;
p->up->down=p->down->up=p;
++num[y];
}
inline void delLR(D *p)
{
p->right->left=p->left;
p->left->right=p->right;
}
inline void delUD(D *p)
{
p->up->down=p->down;
p->down->up=p->up;
}
inline void reLR(D *p)
{
p->left->right=p->right->left=p;
}
inline void reUD(D *p)
{
p->up->down=p->down->up=p;
}
inline void cover(int c)
{
delLR(&col[c]);
for(D *i=col[c].down;i!=&col[c];i=i->down)
{
for(D *j=i->right;j!=i;j=j->right)
--num[j->y],delUD(j);
delLR(i);
}
}
inline void re(int c)
{
for(D *i=col[c].down;i!=&col[c];i=i->down)
{
reLR(i);
for(D *j=i->right;j!=i;j=j->right)
reUD(j),++num[j->y];
}
reLR(&col[c]);
}
bool dance(int k)
{
if(head->right==head)
{
for(int i=1;i<k;++i)
cout<<ans[i]<<' ';
return true;
}
int c=-1,tmp=INF;
for(D *i=head->right;i!=head;i=i->right)
if(num[i->y]<tmp) tmp=num[i->y],c=i->y;
cover(c);
for(D *i=col[c].down;i!=&col[c];i=i->down)
{
i->left->right=i;
for(D *j=i->right;j!=i;j=j->right)
if(j->y) cover(j->y);
i->left->right=i->right;
ans[k]=i->x;
if(dance(k+1)) return true;
i->left->right=i;
for(D *j=i->right;j!=i;j=j->right)
if(j->y) re(j->y);
i->left->right=i->right;
}
re(c);
return false;
}
int main()
{
n=read(),m=read();
init(n,m);
for(int i=1;i<=n;++i)
{
for(int j=1;j<=m;++j)
if(read()) add(i,j);
}
if(!dance(1))
cout<<"No Solution!\n";
}
/*
6 7
0 0 1 0 1 1 0
1 0 0 1 0 0 1
0 1 1 0 0 1 0
1 0 0 1 0 0 0
0 1 0 0 0 0 1
0 0 0 1 1 0 1
*/
/*
4 5 1
*/
后言:
后来用上述方式做POJ3074的时候,有、、慢(超时了)
所以以后还是写下面这种吧:
struct DLX
{
int n,m,cnt;
int U[MAXN],D[MAXN],R[MAXN],L[MAXN],row[MAXN],col[MAXN];
int H[MAXN],S[MAXN];
int ans[MAXN];
void init(int _n,int _m)
{
n=_n,m=_m;
for(int i=0;i<=m;++i)
S[i]=0,U[i]=D[i]=i,L[i]=i-1,R[i]=i+1;
R[m]=0,L[0]=m,cnt=m;
for(int i=1;i<=n;++i)
H[i]=-1;
}
void add(int r,int c)
{
++S[col[++cnt]=c];
row[cnt]=r;
D[cnt]=D[c],U[D[c]]=cnt;
U[cnt]=c,D[c]=cnt;
if(H[r]<0) H[r]=L[cnt]=R[cnt]=cnt;
else R[cnt]=R[H[r]],L[R[H[r]]]=cnt,L[cnt]=H[r],R[H[r]]=cnt;
}
void remove(int c)
{
L[R[c]]=L[c],R[L[c]]=R[c];
for(int i=D[c];i!=c;i=D[i])
for(int j=R[i];j!=i;j=R[j])
U[D[j]]=U[j],D[U[j]]=D[j],--S[col[j]];
}
void resume(int c)
{
for(int i=U[c];i!=c;i=U[i])
for(int j=L[i];j!=i;j=L[j])
++S[col[U[D[j]]=D[U[j]]=j]];
L[R[c]]=R[L[c]]=c;
}
bool dance(int k)
{
if(R[0]==0)
{
/*for(int i=1;i<k;++i)
cout<<ans[i]<<' ';*/
return true;
}
int c=R[0];
for(int i=R[0];i!=0;i=R[i])
if(S[i]<S[c]) c=i;
remove(c);
for(int i=D[c];i!=c;i=D[i])
{
ans[k]=row[i];
for(int j=R[i];j!=i;j=R[j]) remove(col[j]);
if(dance(k+1)) return true;
for(int j=L[i];j!=i;j=L[j]) resume(col[j]);
}
resume(c);
return false;
}
}dlx;