虽然已经退役,但是还是希望能够补一补自己不会的东西,昨天学习了dancing links(舞蹈链),然后做了这个入门级的水题,前后代码重写了好几次,现在算是自己理解了dancing links。
言归正传。
这道题的意思是,从一个01矩阵中选出若干个行,使得这些行组成的矩阵满足恰好每列有且仅有一个1,换句话说,有这些行,恰好可以覆盖每个列
解题思路如下:
1、选出一个列c,然后找能够覆盖这个列的行(列中1元素对应的行都能覆盖该列)
2、对一中找到的每一行,都需要进行dfs(因为不知道选哪一行是对的,也可能有好多答案)
3、在dfs前,因为选中一行之后,该行可能能覆盖好几个列(只要该行中1的个数有好几个即可),那么该行能覆盖的列必须从矩阵中踢出,否则,在后面的列的选择中,如果选到这些列中的一个,就导致了列的重复覆盖,和题意相悖。因此,必须删除2中选出行所能覆盖的所有列,而删除这些列的时候,同时也需要删除能够覆盖这些列的行,因为这些行不可能在后面被选(如果选了,就导致某些列被重复覆盖了)…………………………此话很拗口,本人表示文笔不行,但是思路就是这么回事…………
4、然后dfs
5、回溯,需要回复现场,把之前删掉的东西都补回来
关于dancing links,网上也能搜到资料,在这里大概说一下。
所谓dancing links,其实就是一个双向十字环形链表,在这里我们用数组来表示,U[x]表示x上面的那个节点的下标,D[x]表示下面节点的下标,L[x]表示左边节点的下标,R[x]表示右边节点的下标,所以删除一个节点的操作就是:
R[L[x]] = R[x]; L[R[x]] = L[x]; //回忆一下环形链表删除一个节点
由于删除节点x的时候,并没有清除x的UDLR值(即还是保存着原先x的上下文关系),那么想要将x插回去,只需要:
R[L[x]] = L[R[x]] = x;
有了以上的基础,基本上就可以对dancing links进行操作了,以下附上此题的代码,部分注释已经在代码中给出,在此先说明下某些变量的含义:
R、L、U、D和上文定义一样
C数组,C[x]表示下标为x的那个节点所在的列编号(列的头指针)
Row数组,Row[x]表示下标为x的那个节点所在的行的编号,这个是为了最后记录哪些行被拿掉用的
S数组,S[x]表示列号为x的那个列中剩下的1的个数
O数组,存储拿掉的行编号,为了输出结果用
size,当前最新节点的编号,具体的看代码就可以知道干嘛的
#include <cstdio>
#include <cstring>
const int M = 1010;
const int N = M*M;
int R[N],L[N],U[N],D[N],C[N],Row[N];
int S[M],O[M];
int n,m;
int ans; // 需要拿掉几行
int size; //节点个数
void init()
{
memset(S,0,sizeof(S)); //初始的时候每列上1的个数为0
//链表初始化
for(int i=1; i<=m; i++)
L[i+1] = R[i-1] = U[i] = D[i] = i;
R[m] = 0; //0表示表头
size = m + 1; //新的节点的下标是从m+1开始的,当然也可以随机的设置一个更大的值,没有影响
}
//删除第c列,以及该列中元素对应的所有行
void remove(int c)
{
R[L[c]] = R[c];
L[R[c]] = L[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[C[j]]--;
}
}
//恢复第c列,以及该列中元素对应的所有行
void resume(int c)
{
for(int i=U[c]; i!=c; i=U[i])
for(int j=L[i]; j!=i; j=L[j])
{
U[D[j]] = D[U[j]] = j;
S[C[j]]++;
}
R[L[c]] = L[R[c]] = c;
}
bool dfs(int k) //k表示当前已经拿掉的行数
{
if(R[0]==0) //表示所有的列已经被拿完
{
ans = k;
return 1;
}
int min = M,c=-1;
//选取元素个数最少的列
for(int i=R[0]; i!=0; i=R[i])
if(S[i]<min) min = S[i],c = i;
remove(c);
for(int i=D[c]; i!=c; i=D[i])
{
for(int j=R[i]; j!=i; j=R[j])
remove(C[j]);
O[k] = Row[i]; //当前拿掉的是i那个元素对应的行
if(dfs(k+1)) return 1;
for(int j=L[i]; j!=i; j=L[j])
resume(C[j]);
}
resume(c);
return 0;
}
int main()
{
// freopen("in","r",stdin);
while(scanf("%d%d",&n,&m)==2)
{
init();
for(int i=1; i<=n; i++)
{
int k,x,rowhead=-1;
scanf("%d",&k);
while(k--)
{
scanf("%d",&x);
//在第x列中需要加入一个节点,心节点的编号为size
C[size] = x; //表示该新节点是x列的
//将节点插入到x列的双向链表中
U[size] = U[x];
D[U[x]] = size;
U[x] = size;
D[size] = x;
S[x]++; //x列节点个数+1
//将新节点插入到第i行,如果这个时候第i行的链表还没有头指针,即rowhead==-1,则需要先创建一个头指针,否则,将节点直接插入到链表中
if(rowhead==-1)
{
L[size] = R[size] = size; //左右指针都指向自己,表示链表头
rowhead = size; //链表头的编号即为当前节点的编号
}
else //插入节点
{
L[size] = L[rowhead];
R[L[rowhead]] = size;
L[rowhead] = size;
R[size] = rowhead;
}
Row[size] = i; //当前节点对应的行为第i行
size++; //下次循环时新节点的编号应该是当前节点的编号+1
}
}
if(!dfs(0)) puts("NO");
else
{
printf("%d", ans);
for(int i=0; i<ans; i++) printf(" %d", O[i]);
puts("");
}
}
return 0;
}