舞蹈链解决精确覆盖问题
一、问题引入:
有n 个人, 每个人有一些想吃的菜. 只有你给这个人所有他想吃的菜,他才会吃.
可是你只有m 种菜, 每样一份.你必需把菜卖完. 问最多能满足多少人.
*精确覆盖问题的定义:给定一个由0-1组成的矩阵,是否能找到一个行的集合,使得集合中每一列都恰好包含一个1
假设有5种菜,4个人 ,1表示他喜欢吃这种菜。
(1)0 1 0 1 0
(2)1 0 0 0 0
(3)1 1 1 0 0
(4)0 0 1 0 1
可见最多满足第1 、2 、4。
答案得出的过程。
Step1:我们假设先满足第一人,那么第3人是不能选的(因为每种菜只有一份,3不能和1抢第二种菜)。
Step2: 接着选择第2个人,很完美,两人都可以满足。
Step3:接着选第4个人(第三个人在Step1就淘汰了。)也可以满足他。
这个例子太好了,换一下。
(1)0 0 1 0 1
(2)1 1 1 0 1
(3)0 0 1 1 1
(4)0 0 1 1 1
Step1:发现要覆盖第一列,必须满足第二个人。由于1.3.4人和2都喜欢吃第3种菜,所以都不能满足。
所以只能满足第二个人。但是这时菜不能完全卖完,此问题无解。
总结一下求解的过程:
(1)选择任意一行。
(2)删除选择这一行后不能选择的其他行。
(3)继续选择
(4)若最后没有可选择的行,但是没有完全覆盖,回到第(1)步,修改之前任意选择的行重复步骤。
到这挺好理解的...
二、
发现这过程需要存储矩阵和回溯的过程。那么怎样做比较高效呢。
舞蹈链是一种数据结构,缓存和回溯中效率惊人。不需要额外空间,接近于线性的时间,指针在数据之间
跳跃着,像舞蹈一样所以称之为舞蹈链。(dancing links).
舞蹈链用的数据结构是十字链表。
我们先从双向链表过渡到十字链表吧。
(1)双向链表
A1 A2 A3,
A1.right=A2,A2.left=A1,A3.right.right=A1.
在很多实际运用中,把双向链的首尾相连,构成循环双向链.
删除A2时,A1.right=A3,A3.left=A1,注意A2只是从双向链表中删除了,但是A2的left和right的信息并没有变。
由于不需要开空间,时间也是挺快的。
(2)接下来是双向十字链表。
每个元素有四个指针,left,right,up,down.

浅谈舞蹈链_链表

注意链首和链尾是双向连接的,一定要注意双向。
列首有我们虚构的点,在第0行,这是为了方便搜索,以后代码呈现。

三、用法。

删除和恢复元素。(先了解后看代码)

规则:

沿列删除时删除左右, 保留上下.
沿行删除时删除上下, 保留左右.
恢复时依然沿之前的方向, 根据自己的信息把自己插进去

四、

那么开始的问题引入,我们怎么用舞蹈链来解决呢。

........................................................假装你思考过了的样子.........................orz

首先每一行的行首表示每个人,列表示每个人爱吃的菜,然后求精确覆盖就可以啦。

五、code

当然很恶心。

int l[maxn],r[maxn],u[maxn],d[maxn],tn,ren[maxn],cai[maxn];
void shanchu(int x) {
    for(int p=r[x]; p!=x; p=r[p]) {
        for(int q=u[p]; q!=p; q=u[q]) {
            for(int s=r[q]; s!=q; s=r[s]) {
                if(d[x]!=s) {
                    d[u[s]]=d[s];
                    u[d[s]]=u[s];
                }
            }
        }
    }
}
void huifu(int x) {
    for(int p=l[x]; p!=x; p=l[p]) {
        for(int q=d[p]; q!=p; q=d[q]) {
            for(int s=l[q]; s!=q; s=l[s]) {
                u[d[s]]=d[u[s]]=s;
            }
        }
    }
}
int main() {
    memset(cai,0,sizeof(cai));
    scanf("%d%d",&n,&m);
    ren[0]=tn=1;
    l[1]=r[1]=1;
    d[1]=u[1]=1;
    for(int i = 1; i <= n; i++) {
        ren[i]=++tn;
        l[tn]=r[tn]=tn;//左右方向建表
        d[ren[i-1]]=tn;//上下方向
        u[tn]=ren[i-1];
        d[tn]=1;//指向列首。
        u[1]=tn//列首指向列尾。
             int totcai,caii,lastcai=tn;
        scanf("%d",&totcai);
        for(int j = 0; j < totcai; ++ j) {
            scanf("%d", &caii);
            ++tn;
            r[last = tn;
              l[tn] = lastcai;
              r[tn]=ren[i];
              l[ren[i]]=tn;
              lastcai = tn;
            if(!cai[caii]) { //cai数组指的是caii最后一次出现的位置编号。
            cai[caii]=u[tn]=d[tn]=tn;
            } else {
                u[tn]=cai[caii];
                d[tn]=d[cai[caii]];
                u[d[tn]]=tn;
                d[u[tn]]=tn;
                cai[caii]=tn;
            }
        }
    }
}