二分圖匹配詳解


定義:

對於一個圖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;
} 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM