二分图与一般图的匹配问题(匈牙利算法与带花树算法)

匈牙利算法

匈牙利算法是基于寻找增广路进行的匹配,一条增广路是当然是一条真实存在的路径,由未匹配点开始,以未匹配点结束,匹配点与未匹配点交替出现,这样一条增广路可以让原问题的答案加\(1\)(把匹配边与未匹配边反一反也是合法答案,同时答案更大),具体可以参考这篇博客

所以我们枚举每一个点,去寻找增广路,由于我们寻找的增广路的性质,我们需要一次跳两个点。从当前点开始,找到有连边的点,若它无匹配点,找到一条增广路,然后按原路径返回,把原路径上所有边反一反;否则,搜索它的匹配点是否有增广路。注意,访问过的点没有必要再访问一次了

时间复杂度:\(O(nm)\)

Luogu3386 【模板】二分图最大匹配

\(dfs\)版匈牙利算法

#include<iostream>
#include<cstdio>
#include<algorithm>
#define N 505
#define M 50005
using namespace std;
int n,m,s,tot,x,y,ans,cnt,vis[N],fr[N],nxt[M],d[M],mch[N];
void add(int x,int y)
{
    tot++;
    d[tot]=y;
    nxt[tot]=fr[x];
    fr[x]=tot;
}
bool dfs(int u)
{
    if (vis[u]==cnt)
        return false;
    vis[u]=cnt;
    for (int i=fr[u];i;i=nxt[i])
    {
        int v=d[i];
        if (!mch[v] || dfs(mch[v]))
        {
            mch[v]=u;
            return true;
        }
    }
    return false;
}
int main()
{
    scanf("%d%d%d",&n,&m,&s);
    for (int i=1;i<=s;i++)
    {
        scanf("%d%d",&x,&y);
        add(x,y);
    }
    for (cnt=1;cnt<=n;cnt++)
        if (dfs(cnt))
            ans++;
    printf("%d\n",ans);
    return 0;
}

匈牙利算法同样可以用\(bfs\)实现,原理相同,需要记录整条路径方便随时找到增广路并修改

\(bfs\)版匈牙利算法

#include<iostream>
#include<cstdio>
#include<algorithm>
#define N 505
#define M 50005
using namespace std;
int n,m,s,tot,x,y,ans,q[N],vis[N],fr[N],nxt[M],d[M],mch[N],pr[N],qr[N];
void add(int x,int y)
{
    tot++;
    d[tot]=y;
    nxt[tot]=fr[x];
    fr[x]=tot;
}
bool bfs(int x)
{
    int l=1,r=0;
    q[++r]=x;
    pr[x]=0;
    vis[x]=x;
    while (l<=r)
    {
        int u=q[l];
        l++;
        for (int i=fr[u];i;i=nxt[i])
        {
            int v=d[i];
            if (!mch[v])
            {
                for (int s=u,t=v;s && t;t=pr[s],s=qr[t])
                    mch[t]=s;
                return true;
            } else
            {
                if (vis[mch[v]]==x)
                    continue;
                vis[mch[v]]=x;
                q[++r]=mch[v];
                pr[mch[v]]=v;
                qr[v]=u;
            }
        }
    }
    return false;
}
int main()
{
    scanf("%d%d%d",&n,&m,&s);
    for (int i=1;i<=s;i++)
    {
        scanf("%d%d",&x,&y);
        add(x,y);
    }
    for (int i=1;i<=n;i++)
        if (bfs(i))
            ans++;
    printf("%d\n",ans);
    return 0;
}

带花树算法

带花树算法是处理一般图匹配的有力武器,由处理二分图匹配匈牙利算法优化而来,要了解带花树算法,必须对匈牙利算法算法了如指掌

定义:匹配时如果从\(i\)匹配到\(j\),那么\(i\)为入点,\(j\)为出点

\(pre\)表示入点在增广路中的上一个未匹配节点(非严格,下面在带花树操作中可能会利用它记录一些别的东西)

一般图与二分图的区别在何处呢?奇环!一个仅含有偶环的图可以交替染色成为二分图,而含奇环的图无法避免相邻两个节点的颜色相同

怎么办?考虑我们找增广路的过程,什么时候奇环会成为阻碍呢?

对于长为\(2k+1\)的奇环,倘若上面的取的边的数量小于\(k\)组,那么我们一路增广下去,就会匹配环上另一个未被匹配的点,不会对增广过程产生干扰

而如果奇环恰好匹配了\(k\)对,而一个奇环上顶多只能有\(k\)个匹配,那么我们一路匹配下去,就会把所有的节点都匹配到,而最后一个节点匹配的是入点,入点与入点匹配显然不合法,倘若参照匈牙利算法,将会产生死循环的局面

由于这个原因,带花树算法选择使用\(bfs\)增广,便于处理复杂情况

\(bfs\)一开始所有点都没有染颜色(分为入点和出点),把第一个点作为入点,显然一个入点不会寻找一个出点作为匹配,这相当于偶环,怎么修改都是没有意义的,接下来就是困难的入点与入点匹配的情况了

但是如果我们仔细考虑一下,就会发现,倘若有新的匹配,那么边的性质将会反一反,也就是入点变成出点,出点变成入点,如果这些入点(也就是原来的出点)中有其中一个可以向外匹配(不与环上点匹配)并找到增广路,那么剩下环上\(2k\)个点就可以完全匹配,我们的原问题也找到了增广路

考虑这样一个问题的性质,相当于环上点的匹配情况不可修改,而要从环上的出点找到一条增广路,那么我们缩点即可,缩完的点就称为

那么如何缩环成呢,之所以会有入点与入点匹配,是因为我们从一个节点开始搜索出路径,然后这两条路径碰头了

我们需要找到这个节点,然后让两个节点不断往上跳到该节点,相当于就是修改了整个环,记这个节点为公共祖先\(lca\)\(lca\)一定是入点(我们都是隔一个节点搜的,出点会被跳过),那么这两个节点到\(lca\)的距离都是偶数,可以隔一个节点跳寻找\(lca\),由于我们采用了\(bfs\),这两个节点的深度应该相同,但是中间遍历到的点可能被缩进内,\(x,y\)\(lca\)的距离实际上不一定是相同的,不能\(x,y\)一起跳到\(lca\)

因此,我们可以采取交替跳的方式寻找祖先,第一个跳到的重复点就是\(lca\),由于我们搜完后立刻会把它缩成,所以暴力跳的均摊复杂度为\(O(1)\)

然后遍历整个环,全部用\(pre\)连接起来(相当于\(pre\)连成了一个环,那么如果我们找到了增广路,就可以利用\(pre\)从任意位置开始重构原图的增广路)还记得入点变成出点,出点变成入点吗,首先所有的出点都会变成入点,并加入队列(它们有寻找增广路的任务),同时,为了避免没有意义的再次访问,修改这个环(从任意一个点进入都会交替染色,情况仅有两种,这两种情况我们都已经搜索了,不需要再次重复搜索),我们不改变入点的颜色,也就是说一朵花内的点全是入点,整朵花缩成了一个入点

注意,由于存在花套花的情况,我们必须借助并查集才能知道一个节点的在中的真正祖先

带花树中的并查集如果记录了真正祖先,可以加上按秩合并优化

以上就是带花树算法的流程

时间复杂度:\(O(nm \alpha(n))\)

Luogu6113 【模板】一般图最大匹配

#include<iostream>
#include<cstdio>
#include<algorithm>
#define N 1005
#define M 50005
using namespace std;
int n,m,x,y,tot,ans,fr[N],d[M << 1],nxt[M << 1];
int l,r,tim,f[N],col[N],pre[N],mch[N],q[N],vis[N],dfn[N];
void add(int x,int y)
{
    tot++;
    d[tot]=y;
    nxt[tot]=fr[x];
    fr[x]=tot;
}
int getf(int x)
{
    return (f[x]==x)?x:(f[x]=getf(f[x]));
}
int lca(int x,int y)
{
    tim++;
    x=getf(x),y=getf(y);
    while (dfn[x]!=tim)
    {
        dfn[x]=tim;
        x=getf(pre[mch[x]]);
        if (y)
            swap(x,y);
    }
    return x;
}
void Blossom(int x,int y,int rt)
{
    while (getf(x)!=rt)
    {
        pre[x]=y,y=mch[x];
        if (col[y]==2)
            col[y]=1,q[++r]=y;
        if (getf(x)==x)
            f[x]=rt;
        if (getf(y)==y)
            f[y]=rt;
        x=pre[y];
    }
}
bool bfs(int x)
{
    for (int i=1;i<=n;i++)
        f[i]=i,col[i]=pre[i]=0;
    l=1,r=0;
    q[++r]=x;
    col[x]=1;
    while (l<=r)
    {
        int u=q[l];
        l++;
        for (int i=fr[u];i;i=nxt[i])
        {
            int v=d[i];
            if (getf(u)==getf(v) || col[v]==2)
                continue;
            if (!col[v])
            {
                pre[v]=u,col[v]=2;
                if (!mch[v])
                {
                    int lst;
                    for (int z=v;z;z=lst)
                        lst=mch[pre[z]],mch[z]=pre[z],mch[pre[z]]=z;
                    return true;
                }
                col[mch[v]]=1,q[++r]=mch[v];
            } else
            {
                int w=lca(u,v);
                Blossom(u,v,w);
                Blossom(v,u,w);
            }
        }
    }
    return false;
}
int main()
{
    scanf("%d%d",&n,&m);
    for (int i=1;i<=m;i++)
    {
        scanf("%d%d",&x,&y);
        add(x,y),add(y,x);
    }
    for (int i=1;i<=n;i++)
    {
        if (mch[i])
            continue;
        if (bfs(i))
            ans++;
    }
    printf("%d\n",ans);
    for (int i=1;i<=n;i++)
        printf("%d ",mch[i]);
    putchar('\n');
    return 0;
}
posted @ 2020-09-14 15:15  GK0328  阅读(170)  评论(0编辑  收藏  举报