BZOJ4405: [wc2016]挑战NPC 一般图最大匹配

本文详细介绍了一种解决特定匹配问题的带花树算法。该算法适用于将球放入筐子的问题,目标是最大化半空筐子的数量。文章通过实例解释了如何构建图模型,并运用带花树算法求解。此外,还提供了实现该算法的代码示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

有n个球,用整数1到n编号。还有m个筐子,用整数1到m编号。
每个筐子最多能装3个球。
每个球只能放进特定的筐子中。具体有e个条件,第i个条件用两个整数vi和ui描述,表示编号为vi的球可以放进编号为ui的筐子中。
每个球都必须放进一个筐子中。如果一个筐子内有不超过1个球,那么我们称这样的筐子为半空的。
求半空的筐子最多有多少个,以及在最优方案中,每个球分别放在哪个筐子中。
注:BZ上输出与题意不符,代码是UOJ上的
T≤5,m≤100,1≤n≤3m
再不写博客就快忘了这题怎么做了*2
啊呀好像不是快忘了,是已经忘没了。。。先去看一眼代码。。。
(详细说明在代码后面)

#include<cstdio>
#include<cstring>
#include<queue>
#define gm 601
using namespace std;
struct blossom_tree
{
    struct e
    {
        int t;
        e *n;
        e(int t,e *n):t(t),n(n){}
        ~e(){delete n;}
    }*f[gm];
    void add_edge(int x,int y)
    {
        f[x]=new e(y,f[x]);
        f[y]=new e(x,f[y]);
    }
    struct union_set
    {
        int r[gm];
        union_set():r(){}
        void init(int n)
        {
            for(int i=1;i<=n;++i)
            r[i]=i;
        }
        int find(int x)
        {
            return r[r[x]]==r[x]?r[x]:r[x]=find(r[x]);
        }
        void attach(int a,int b)
        {
            r[find(a)]=find(b);
        }
        bool unicom(int a,int b)
        {
            return find(a)==find(b);
        }
        bool is_root(int x)
        {
            return find(x)==x;
        }
    }s;
    queue<int> q;
    int mat[gm],pre[gm],n;
    char st[gm];
    int& operator [] (size_t x) {return mat[x];}
    blossom_tree(int n):f(),s(),q(),mat(),pre(),n(n),st(){}
    ~blossom_tree()
    {
        for(int i=1;i<=n;++i)
        delete f[i];
    }
    void init()
    {
        s.init(n);
        memset(st,-1,n+1);
        q=queue<int>();
    }
    bool saku(int x)
    {
        int y;
        while(x)
        {
            y=mat[pre[x]];
            mat[pre[x]]=x;
            mat[x]=pre[x];
            x=y;
        }
        return 1;
    }
    int lca(int x,int y)
    {
        static int tm[gm]={},ct=0;
        ++ct;
        while(x)
        {
            tm[x=s.find(x)]=ct;
            x=pre[mat[x]];
        }
        while(y)
        {
            if(tm[y=s.find(y)]==ct) return y;
            y=pre[mat[y]];
        }
        throw 0;
    }
    void shrink(int x,int y,int r)
    {
        while(!s.unicom(x,r))
        {
            pre[x]=y;
            y=mat[x];
            if(st[y]==1) st[y]=0,q.push(y);
            if(s.is_root(x)) s.attach(x,r);
            if(s.is_root(y)) s.attach(y,r);
            x=pre[y];
        }
    }
    bool aug(int from)
    {
        init();
        pre[from]=st[from]=0;
        q.push(from);
        while(!q.empty())
        {
            int now=q.front();q.pop();
            for(e *i=f[now];i;i=i->n)
            {
                int to=i->t;
                if(st[to]==-1)
                {
                    pre[to]=now;
                    if(!mat[to]) return saku(to);
                    st[to]=1,st[mat[to]]=0;
                    q.push(mat[to]);
                }
                else if(st[to]==0&&!s.unicom(now,to))
                {
                    int __lca=lca(now,to);
                    shrink(now,to,__lca);
                    shrink(to,now,__lca);
                }
            }
        }
        return 0;
    }
    int mate()
    {
        int ans=0;
        for(int i=n;i;--i)
        if(!mat[i]&&aug(i))
        ++ans;
        return ans;
    }
};
int t,n,m,e;
int main()
{
    scanf("%d",&t);
    while(t--)
    {
        scanf("%d%d%d",&n,&m,&e);
        blossom_tree &tr=*new blossom_tree(3*m+n);
        for(int i=1;i<=m;++i)
        tr.add_edge(i,m+i);
        for(int i=1;i<=e;++i)
        {
            int u,v;
            scanf("%d%d",&u,&v);
            tr.add_edge(3*m+u,v);
            tr.add_edge(3*m+u,m+v);
            tr.add_edge(3*m+u,2*m+v);
        }
        printf("%d\n",tr.mate()-n);
        for(int j=3*m+1;j<=3*m+n;++j)
        printf("%d ",(tr[j]-1)%m+1);
        puts("");
        delete &tr;
    }
    return 0;
}

我写的这什么玩意

好在当时就预感到以后会看不懂所以尽量写得比较明白。。。大概梳理了一下代码的意思:
和二分图匹配一样,依次枚举每个未匹配的点,判断从这个点出发是否能找到一条增广路,若能则将上面所有边的匹配情况取反,匹配数增加1
把每个点分为S和T两类,也就是代码中的st数组,意义是:
-1:这个点尚未被访问到
0:S类点,正在试图向外找到一个配偶来替换原来的配偶
1:T类点,有人正试图匹配他,但他必须让原来的配偶找到新的归宿才能跟从别人,也就是说他的配偶是S型点。
初始点是S型点,存在于队列中的点都是S型点,增广过程构成了一棵以初始点为根的BFS树。
另外还有一个数组pre,代表这个节点所连接的非匹配边,找到可以增广的点后,沿着mat-pre构成了一条到根的增广路。如果没有奇环的话,mat-pre就相当于树上的树边,但是当有环存在时就比较麻烦了,要将环缩成大点(即花)。
于是带花树算法的增广流程如下:
初始化后将初始点设为S点并加入队列进行BFS。对于能访问到的点:
若之前未访问过,若没有匹配点那么就找到了增广路,否则它是T点,它的配偶是S点,将配偶加入队列。
若它是T点,那么它对应的S点已经加入过队列中了,不用管他。
若是S点,说明两个S点之间有边相连,也就是找到了一个奇环。如果这个奇环尚未被缩到同一个点里,那么就要进行缩点。
然后就是带花树的精髓操作了:奇环上的每一个点都有可能向外拓展增广路,因此不仅要将其全部变为S点,还要向环的另一侧设置pre,通过改变pre的走向确保每一个S点都可以通过跳mat-pre来回到根。
具体地,我们分别将a和b到其lca的路径进行收缩操作,将一个点不停跳mat-pre直到他与根在同一个花里。对于点x,其mat为y,若y为T点,则将y设为S点并加入队列。之后x变为pre[y],且由于环的缘故,pre[x]设成y,画一下图就可以明白这是为了使环上每个点顺着mat-pre都能够走偶数步到达环的端点,而环的端点的mat-pre即为树边。另外,中途经过的点若是并查集的根(也就是某个花的端点),那么将其并到要缩的点里。
注意细节,必须像上面代码中那样先判断是否已经与lca在同一花内再连pre,now和to之间的pre边也是这样。
找a与b的lca的话,先从a出发每次跳到花的端点,将其盖上时间戳,走一次mat-pre到达祖先,再接着跳到花的端点,如此往复,直到为0。然后再用b这么跳,找到的第一个带当前时间标记的就是lca。
增广的话,将最后一个自由点也用pre连上,就成了一条pre开头pre结尾的路径,每次对于pre的两端点,将他们互设为mat,然后跳到原来mat的另一端。
至此带花树的主要操作就完成了,细节巨多而且错一点就GG,建议看代码。
话说是不是还没有说这题怎么做。。。
每个篮子拆成三个点,其中两个点连一条边。每个球与能放它的篮子的三个点各连一条边。这样如果一个篮子的三个点只匹配了0或1个,内部就可以产生1的贡献,于是最后最大匹配减去球数即可。
不过如果需要输出方案的话,假设有两个球一个篮子,这两个球各匹配篮子和一个球匹配篮子,篮子内部匹配,这两个的匹配数一样,但后者是不合法的。考虑一个匹配不会被不比它更优的匹配所替换,我们先只保留球到篮子的边进行匹配,得到一个将球都放进篮子的匹配方案,再加入其他边进行匹配,就能保证结果合法啦
(实际上对于这道题只要先从球出发找增广路就可以把球都放进篮子,不需要分两次加边。)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值