二分图与一般图的匹配问题(匈牙利算法与带花树算法)
匈牙利算法
匈牙利算法是基于寻找增广路进行的匹配,一条增广路是当然是一条真实存在的路径,由未匹配点开始,以未匹配点结束,匹配点与未匹配点交替出现,这样一条增广路可以让原问题的答案加\(1\)(把匹配边与未匹配边反一反也是合法答案,同时答案更大),具体可以参考这篇博客
所以我们枚举每一个点,去寻找增广路,由于我们寻找的增广路的性质,我们需要一次跳两个点。从当前点开始,找到有连边的点,若它无匹配点,找到一条增广路,然后按原路径返回,把原路径上所有边反一反;否则,搜索它的匹配点是否有增广路。注意,访问过的点没有必要再访问一次了
时间复杂度:\(O(nm)\)
\(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))\)
#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;
}