二分图匹配详解

定义:

对于一个图G=(V,E),若能将其点集分为两个互不相交的两个子集X、Y, 使得X∩Y=∅,且对于G的边集V,若其所有边的顶点全部一侧属于X,一侧属于Y,则称图G为一个二分图。

所以当且仅当无向图G的回路个数为偶数时,图G为一个二分图。无回路的图也是二分图。

判定:

在二分图G中,任选一个点V, 使用BFS算出其他点相对于V的距离(边权为1)对于每一条边E,枚举它的两个端点,若其两个端点的值, 一个为奇数,一个为偶数,则图G为一个二分图。

一、二分图最大匹配(Luogu P3386 【模板】二分图匹配

1.匈牙利算法

dalao:随便一个ISAP加前弧优化跑的飞快!

juruo:我们不用网络流,学不起

这是我的真实写照qaq

匈牙利算法就是一个协商与匹配的过程。

算法的主要步骤为:

1.首先从任意的一个未配对的点u开始,从点u的边中任意选一条边(假设这条边是从u->v)开始配对。如果点v未配对,则配对成功,这是便找到了一条增广路。如果点v已经被配对,就去尝试“连锁反应”,如果这时尝试成功,就更新原来的配对关系。所以这里要用一个matched[v] = u。配对成功就将配对数加1,。

2.如果刚才所选的边配对失败,那就要从点u的边中重新选一条边重新去试。直到点u 配对成功,或尝试过点u的所有边为止。

3.接下来就继续对剩下的未配对过的点一一进行配对,直到所有的点都已经尝试完毕,找不到新的增广路为止。

代码实现

#include <bits/stdc++.h>
#define N 1005
#define M 1000005
using namespace std;
inline int read()
{
    register int x=0,f=1;register char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9')x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
    return x*f;
}
inline void write(register int x)
{
    if(!x)putchar('0');if(x<0)x=-x,putchar('-');
    static int sta[25];int tot=0;
    while(x)sta[tot++]=x%10,x/=10;
    while(tot)putchar(sta[--tot]+48);
}
struct node{
    int to,next;
}e[M];
int head[N],tot=0;
inline void add(register int u,register int v)
{
    e[++tot]=(node){v,head[u]};
    head[u]=tot;
}
int n,m,es,ans;
int ask[N<<1],matched[N<<1];
inline bool found(register int x)
{
    for(register int i=head[x];i;i=e[i].next)
    {
        int v=e[i].to;
        if(ask[v])
            continue;
        ask[v]=1;
        if(!matched[v]||found(matched[v]))
        {
            matched[v]=x;
            return true;
        }
    }
    return false;
}
inline void match()
{
    for(register int i=1;i<=n;++i)
    {
        memset(ask,0,sizeof(ask));
        if(found(i))
            ++ans;
    }
}
int main()
{
    n=read(),m=read(),es=read();
    while(es--)
    {
        int u=read(),v=read();
        if(v>m)
            continue;
        add(u,v+n);	
    }	
    match();
    write(ans);
    return 0;
} 

匈牙利算法的复杂度为\(O(ne)\)

2.HK(Hopcroft-Karp港记)算法

这个算法的效率比匈牙利算法的效率高,复杂度为\(O(\sqrt n e)\),但很容易写挂,不适合考场上写(蒟蒻的想法)

该算法的主要思想是在每次增广的时候不是找一条增广路而是同时找几条不相交的最短增广路,形成极大增广路集,随后可以沿着这几条增广路同时进行增广。

可以证明在寻找增广路集的每一个阶段所寻找到的最短增广路都具有相等的长度,并且随着算法的进行最短增广路的长度是越来越长的,更进一步的分析可以证明最多只需要增广ceil(sqrt(n))次就可以得到最大匹配(证明在这里略去)。

因此现在的主要难度就是在O(e)的时间复杂度内找到极大最短增广路集,思路并不复杂,首先从所有X的未盖点进行BFS,BFS之后对每个X节点和Y节点维护距离标号,如果Y节点是未盖点那么就找到了一条最短增广路,BFS完之后就找到了最短增广路集,随后可以直接用DFS对所有允许弧(dist[y]=dist[x]+1)进行类似于匈牙利中寻找增广路的操作,这样就可以做到O(e)的复杂度。

根据程序理解更佳(代码细节较多)

/*
dx[i]表示左集合i顶点的距离编号
dy[i]表示右集合i顶点的距离编号
mx[i]表示左集合顶点所匹配的右集合顶点序号
my[i]表示右集合i顶点匹配到的左集合顶点序号
*/
#include <bits/stdc++.h>
#define N 1005
#define M 1000005
using namespace std;
inline int read()
{
    register int x=0,f=1;register char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9')x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
    return x*f;
}
inline void write(register int x)
{
    if(!x)putchar('0');if(x<0)x=-x,putchar('-');
    static int sta[25];int tot=0;
    while(x)sta[tot++]=x%10,x/=10;
    while(tot)putchar(sta[--tot]+48);
}
struct node{
    int to,next;
}e[M];
int head[N],tot=0;
inline void add(register int u,register int v)
{
    e[++tot]=(node){v,head[u]};
    head[u]=tot;
}
int n,m,es,ans;
int dx[N],dy[N],mx[N],my[N],vis[N],dis;
inline bool bfs()
{
    queue<int> q;
    dis=1926081700;
    memset(dx,-1,sizeof(dx));
    memset(dy,-1,sizeof(dy));
    for(register int i=1;i<=n;++i)
        if(mx[i]==-1)
        {
            q.push(i);
            dx[i]=0;
        }
    while(!q.empty())
    {
        int u=q.front();
        q.pop();
        if(dx[u]>dis)
            break;
        for(register int i=head[u];i;i=e[i].next)
        {
            int v=e[i].to;
            if(dy[v]==-1)
            {
                dy[v]=dx[u]+1;
                if(my[v]==-1)
                    dis=dy[v];
                else
                {
                    dx[my[v]]=dy[v]+1;
                    q.push(my[v]);
                        }		
            }	
        }
    }
    return dis!=1926081700;
}
inline bool dfs(register int u)
{
    for(register int i=head[u];i;i=e[i].next)
    {
        int v=e[i].to;
        if(vis[v]||(dy[v]!=dx[u]+1))
            continue;
        vis[v]=1;
        if(my[v]!=-1&&dy[v]==dis)
            continue;
        if(my[v]==-1||dfs(my[v]))
        {
            my[v]=u;
            mx[u]=v;
            return true;
        }
    }
    return false;
}
inline void match()
{
    memset(mx,-1,sizeof(mx));
    memset(my,-1,sizeof(my));
    while(bfs())
    {
        memset(vis,0,sizeof(vis));
        for(register int i=1;i<=n;++i)
            if(mx[i]==-1&&dfs(i))
                ++ans;
    }
}
int main()
{
    n=read(),m=read(),es=read();
    while(es--)
    {
        int u=read(),v=read();
        if(v>m)
            continue;
        add(u,v);	
    }	
    match();
    write(ans);
    return 0;
} 

3.最大流

普通Dinic来二分图匹配的话复杂度是\(O(n \sqrt e)\)

比较模板,适合考场上写(如果会ISAP,HLPP,前弧优化的话),也珂以使程序速度更快

懒着写了(洛咕上题解有的比匈牙利慢,有的会WA,也找不到好的

相关题目:

1.Luogu P2319 [HNOI2006]超级英雄

二分图最大匹配模板题

二、二分图最大权匹配

1.KM(Kuhn-Munkras)算法

这个算法有局限性,只能在带权最大匹配一定是完备匹配的情况下才能使用

完备匹配:给定一张二分图,左部右部节点数都是N。如果二分图的最大匹配包含N条匹配边,则称该二分图具有完备匹配

此算法复杂度是\(O(n^3)\)

珂以看一下这篇文章

还珂以看一下这篇文章

我们以Luogu P4014 分配问题作为模板给一下KM的写法

跑两次就行,一次边权为正,一次边权为负,答案就是顶标之和,边权为负是要记得把答案取反。

#include <bits/stdc++.h>
#define N 105
using namespace std;
inline int read()
{
	register int x=0,f=1;register char ch=getchar();
	while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
	while(ch>='0'&&ch<='9')x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
	return x*f;
}
inline void write(register int x)
{
	if(!x)putchar('0');if(x<0)x=-x,putchar('-');
	static int sta[25];int tot=0;
	while(x)sta[tot++]=x%10,x/=10;
	while(tot)putchar(sta[--tot]+48);
}
inline int Max(register int x,register int y)
{
	return x>y?x:y;
}
inline int Min(register int x,register int y)
{
	return x<y?x:y;
}
int len[N][N],n;
int lx[N],ly[N],link[N];
int s[N],t[N];
inline bool dfs(register int x)
{
	s[x]=1;
	for(register int i=1;i<=n;++i)
		if(lx[x]+ly[i]==len[x][i]&&!t[i])
		{
			t[i]=1;
			if(!link[i]||dfs(link[i]))
			{
				link[i]=x;
				return true;
			}
		}
	return false;
}
inline void update()
{
	register int a=1<<30;
	for(register int i=1;i<=n;++i)
		if(s[i])
			for(register int j=1;j<=n;++j)
				if(!t[j])
					a=Min(a,lx[i]+ly[j]-len[i][j]);
	for(register int i=1;i<=n;++i)
	{
		if(s[i])
			lx[i]-=a;
		if(t[i])
			ly[i]+=a;
	}
}
inline void KM()
{
	for(register int i=1;i<=n;++i)
	{
		link[i]=lx[i]=ly[i]=0;
		for(register int j=1;j<=n;++j)
			lx[i]=Max(lx[i],len[i][j]);
	}
	for(register int i=1;i<=n;++i)
		while(19260817)
		{
			for(register int j=1;j<=n;++j)
				s[j]=t[j]=0;
			if(dfs(i))
				break;
			else
				update();
		}
}
int main()
{
	n=read();
	for(register int i=1;i<=n;++i)
		for(register int j=1;j<=n;++j)
			len[i][j]=read();
	int ans1=0,ans2=0;
	KM();
	for(register int i=1;i<=n;++i)
		ans1+=lx[i]+ly[i];
	for(register int i=1;i<=n;++i)
		for(register int j=1;j<=n;++j)
			len[i][j]*=-1;
	KM();
	for(register int i=1;i<=n;++i)
		ans2+=lx[i]+ly[i];
	write(-ans2),puts(""),write(ans1);
	return 0;
 } 

2.费用流

建个超级源点和超级汇点,跑最小(最大)费用最大流

也懒着写了qaq

相关题目:

1.Luogu UVA1411 Ants

二分图最大权匹配和基础几何

三、二分图多重匹配

1.拆点

和拆完点跑最大流

2.二分图多重匹配算法

这个算法没有具体的名称

实际就是用结构体表示匹配的点qaq

剩下的和匈牙利一样(HK应该也行)

我们以Luogu UVA1345 Jamie's Contact Groups为例讲一下二分图多重匹配算法

题意:Jamie有很多联系人,但是很不方便管理,他想把这些联系人分成组,已知这些联系人可以被分到哪个组中去,而且要求每个组的联系人上限最小,即有一整数k,使每个组的联系人数都不大于k,问这个k最小是多少?

一对多的二分图的多重匹配。二分图的多重匹配算法的实现类似于匈牙利算法,对于集合x中的元素\(x_i\),找到一个与其相连的元素\(y_i\)后,检查匈牙利算法的两个条件是否成立,若\(y_i\)未被匹配,则将\(x_i\)\(y_i\)匹配。否则,如果与\(y_i\)匹配的元素已经达到上限,那么在所有与\(y_i\)匹配的元素中选择一个元素,检查是否能找到一条增广路径,如果能,则让出位置,让\(x_i\)\(y_i\)匹配。

上限最小,一看就是二分答案

完整代码

#include <bits/stdc++.h>
#define N 1005 
using namespace std;
inline void write(register int x)
{
	if(!x)putchar('0');if(x<0)x=-x,putchar('-');
	static int sta[25];register int tot=0;
	while(x)sta[tot++]=x%10,x/=10;
	while(tot)putchar(sta[--tot]+48);
}
struct node{
	int to,next;
}e[N*N];
int head[N],tot=0;
inline void add(register int u,register int v)
{
	e[++tot]=(node){v,head[u]};
	head[u]=tot;
}
int vis[N];
struct match{
	int cnt,k[N];
}link[N];
int n,m;
inline bool dfs(register int u,register int limit)
{
	for(register int i=head[u];i;i=e[i].next)
		if(!vis[e[i].to])
		{
			int v=e[i].to;
			vis[v]=1;
			if(link[v].cnt<limit)
			{
				link[v].k[link[v].cnt++]=u;
				return true;
			}
			for(register int j=0;j<link[v].cnt;++j)
				if(dfs(link[v].k[j],limit))
				{
					link[v].k[j]=u;
					return true;
				}
		}
	return false;
}
inline bool match(register int limit)
{
	memset(link,0,sizeof(link));
	for(register int i=1;i<=n;++i)
	{
		memset(vis,0,sizeof(vis));
		if(!dfs(i,limit))
			return false;
	}
	return true;
}
int main()
{
	char s[20],ch;
	int x;
	scanf("%d%d",&n,&m);
	while(!(n==0&&m==0))
	{
		memset(head,0,sizeof(head));
		tot=0;
		for(register int i=1;i<=n;++i)
		{
			scanf("%s",s);
			while(19260817)
			{
				scanf("%d%c",&x,&ch);
				add(i,x+1);
				if(ch=='\n')
					break;
			}
		}
		int L=1,R=n,ans=n;
		while(L<=R)
		{
			int mid=L+R>>1;
			if(match(mid))
			{
				R=mid-1;
				ans=mid;
			}
			else
				L=mid+1;
		}
		write(ans),puts("");
		scanf("%d%d",&n,&m);
	}
	return 0;
} 
posted @ 2018-12-06 21:12  JSOI爆零珂学家yzhang  阅读(2411)  评论(0编辑  收藏  举报