淺談 KM 算法


KM 算法,全名 Kuhn-Munkres 算法,可以在 \(O(n^3)\)​ 時間內求出二分圖的最大權完美匹配。

該算法的核心思想是給每個點一個頂標 \(l_i\),使得 \(\forall(u,v),l_u+l_v\ge w_{u,v}\),匹配時只考慮滿足 \(l_u+l_v=w_{u,v}\) 的邊 \(u,v\),這樣可以使得匹配時僅考慮這些邊,不用考慮邊權跑出來的完美匹配一定是最大權完美匹配。證明考慮任意一個完美匹配權值為 \(\sum w(u,v)\le\sum l\),而我們按照上述方法跑出來的匹配權值為 \(\sum l\)

考慮如何實現。我們先初始化一組頂標,使得其滿足上述條件,一種可行的初始化方案是令右部點的頂標 \(l_v=0\),左部點的頂標 \(l_u=\max_v w_{u,v}\)。這之后,我們只考慮所有點和滿足 \(l_u+l_v=w_{u,v}\)​ 的邊,我們稱這樣的一個子圖為“相等子圖”,考慮在其中找匹配。

仿照找最大匹配的方法,我們考慮每次新加一個左部點進入匹配。加入一個左部點 \(u\) 時,我們從 \(u\) 開始跑一遍匈牙利算法找匹配,若找到匹配則繼續考慮下一個左部點,若未找到匹配則說明當前相等子圖已經無法找到完美匹配,此時我們要設法擴大它,於是我們考慮調整頂標。

注意到此時從 \(u\) 開始通過走未匹配、匹配、未匹配...的交錯路我們可以到達部分左部點,將這些路徑提出來可以得到一棵樹,我們稱之為交錯樹。定義在交錯樹上的左/右部點分別為點集 \(S,T\),不在交錯樹上的點類似定義為 \(S',T'\)​。

下圖是一個例子,其中紅色點是 \(u\)​​​,紅色邊是匹配中的邊,所有邊都是相等子圖中的邊。在這個例子中,交錯樹是一條鏈(左 4 到左 3)。

我們發現:一定沒有 \(S-T'\) 的邊,不然交錯樹會增長或者甚至找到匹配;\(S'-T\)​​ 的邊一定是非匹配邊,否則該左部點將可以被 \(u\) 走到,從而進入 \(S\)​。

考慮一種調整的操作:設定一個常數 \(a\)​​,給 \(S\)​​ 中的頂標 \(-a\)​​,\(T\)​​ 中的頂標 \(+a\)​​​,則該操作會造成以下效果:

  • \(S-T,S'-T'\) 的邊不受影響。
  • \(S'-T\)​​ 的邊 \(l_u+l_v\)​ 增大,可能會有邊被移出相等子圖。
  • \(S-T'\) 的邊 \(l_u+l_v\) 減小,可能有邊被加入相等子圖。

然而此時我們選的這個 \(a\) 需要使得我們做完這次操作后仍然對於所有邊有 \(l_u+l_v\ge w_{u,v}\)。第一種影響不用考慮,第二種影響的 \(l_u+l_v\)​​​​ 只增不減,於是也不用考慮,只用考慮第三種影響。為了使調整后的頂標仍然滿足條件,我們取 \(a=\min\{l_u+l_v-w_{u,v}\mid u\in S,v\in T'\}\) 即可。

調整后會有新右部點加入,我們考慮這個右部點:

  • 如果其是未匹配點,則我們已經找到了一條增廣路。
  • 如果其與 \(S'\) 中的點匹配了,則我們的 \(S,T\) 集合與交錯樹均被擴大,我們繼續重復剛才的過程即可。

可以發現,在最多 \(n\) 次操作后,我們一定可以找到一個未匹配點,於是此時匹配完成,繼續考慮下一個左部點即可。

實現時直接維護交錯樹即可,因為交錯樹中的邊是只增不刪的。

除此之外,為了保證復雜度,我們還需要對每個 \(T'\)​​ 中的節點維護一個松弛量 \(slack_v=\min \{l_u+l_v-w_{u,v}\mid u\in S\}\)​​,每次求 \(a\)​​ 的值時暴力枚舉每一個 \(T'\)​​​ 中的點,找到最小的 \(slack_v\),令 \(a\) 等於其值,並把 \(v\)​ 和其匹配點(如果有)加入交錯樹即可,而無需再將能從之直接到達的點加入:因為若 \(v\) 的匹配點 \(x\) 還能到達其他右部點 \(y\),則說明有 \(l_x+l_y-w_{x,y}=0\),根據定義有 \(slack_y=0\)​,於是在接下來的操作中 \(y\) 會先被加入。

時間復雜度:需要執行 \(O(n)\) 次加入左部點操作,每次操作需要跑一遍 \(O(n^2)\) 的匈牙利算法,每次需要將 \(O(n)\) 個左部點加入交錯樹中,每加入一個左部點需要 \(O(n)\) 用當前加入的點對每個右部點計算 \(slack\);每次操作要調整 \(O(n)\) 次頂標,每調整一次頂標要修改 \(O(n)\) 個點的頂標和 \(slack\),於是時間復雜度是嚴格 \(O(n^3)\)

對於更一般的情況的處理:

  • 若原圖左右部點數量相等但並不保證有完美匹配,需分情況討論:

    • 若只要求最大權或是允許部分點不匹配等情況,可以把原圖補成完全圖,其中連的虛邊邊權均為 0;
    • 若要求最大匹配,有兩種方法:一種是網上介紹的多的方法是補成完全圖,其中邊權為負無窮;一種是我口胡的(不知道對不對),可以先把原圖跑一遍最大匹配看看是否有完美匹配。
  • 若左右部點數量不等,可以通過補虛點的方式使左右部點數量相等,然后用上面的方法做。

code(洛谷模板題):

#include <bits/stdc++.h>
using namespace std;
int n , m , x , y , z , fp[1100] , par[1100] , fa[1100] , mn[1100] , ok = 0;
int fst[1100] , nex[550000] , v[550000] , tot , mxpar[1100] , f[550][550];
long long l[1100] , slack[1100] , ans , val[550000];
vector< int > id , lp , rp;
void add( int a , int b , long long c )
{
	nex[++tot] = fst[a]; fst[a] = tot;
	v[tot] = b; val[tot] = c;
}
int match( int u )
{
	id.push_back(u);
	for(int i = fst[u] ; i ; i = nex[i] )
	{
		if(l[u] + l[v[i]] != val[i] || fa[v[i]]) continue;
		if(!fp[v[i]]) 
		{
			par[u] = v[i]; par[v[i]] = u; fp[u] = fp[v[i]] = 1;
			ans += l[v[i]];
			return 1;
		}
		else
		{
			if(fa[v[i]] = u , fa[par[v[i]]] = v[i] , match(par[v[i]])) 
			{
				par[v[i]] = u; par[u] = v[i]; fp[u] = fp[v[i]] = 1;
				return 1;
			}
		}
	}
	return 0;
}
long long KM( vector< int > &lp , vector< int > &rp )
{
	ans = 0;
	int t = n + n;
	while(lp.size() < rp.size()) 
	{
		lp.push_back(++t);
		for(int i : rp) add(t , i , 0);
	}
	while(lp.size() > rp.size()) 
	{
		rp.push_back(++t);
		for(int i : lp) add(i , t , 0);
	}
	for(int i : lp) fp[i] = l[i] = par[i] = 0;
	for(int i : rp) fp[i] = l[i] = par[i] = 0;
	for(int i : lp)
		for(int e = fst[i] ; e ; e = nex[e] ) l[i] = max(l[i] , (long long)val[e]);
	for(int i : lp)
	{
		for(int u : lp) fa[u] = 0 , slack[u] = 1e16;
		for(int u : rp) fa[u] = 0 , slack[u] = 1e16;
		slack[0] = 1e16;
		fa[i] = -1; 
		if(match(i)) 
		{ 
			ans += l[i] , id.clear(); 
			continue; 
		}
		while(1)
		{
			for(int j : id)
				for(int e = fst[j] ; e ; e = nex[e] )
					if(slack[v[e]] > l[j] + l[v[e]] - val[e])
						slack[v[e]] = l[j] + l[v[e]] - val[e] , mn[v[e]] = j;
			id.clear();
			int u = 0;
			for(int j : rp)
				if(slack[j] < slack[u] && !fa[j]) u = j;
			int w = slack[u];
			for(int j : lp)
				if(fa[j]) l[j] -= w , ans -= w;
			ans += w;
			for(int j : rp)
			{
				if(fa[j]) l[j] += w , ans += w;
				else slack[j] -= w;
			}
			if(!fp[u])
			{
				ans += l[u] + l[i]; 
				int las = u; u = mn[u]; 
				while(u != -1)
				{
					if(las)
					{
						par[las] = u; par[u] = las; fp[u] = fp[las] = 1;
						las = 0;
					}
					else las = u;
					u = fa[u];
				}
				break;
			}
			fa[u] = mn[u]; int v = par[u]; fa[v] = u;
			id.push_back(v);
		}
	}
	return ans;
}
int main()
{
//	freopen("1.in" , "r" , stdin);
//	freopen("1.out" , "w" , stdout);
	scanf("%d%d" , &n , &m);
	for(int i = 1 ; i <= m ; i++ )
	{
		scanf("%d%d%d" , &x , &y , &z); f[x][y] = 1; z += 19980731;
		add(x , y + n , z); 
	}
	for(int i = 1 ; i <= n ; i++ )
		for(int j = n + 1 ; j <= n + n ; j++ )
			if(!f[i][j - n]) add(i , j , -1e12);
	for(int i = 1 ; i <= n ; i++ ) lp.push_back(i);
	for(int i = n + 1 ; i <= n + n ; i++ ) rp.push_back(i);
	printf("%lld\n" , KM(lp , rp) - 19980731ll * n);
	for(int i = n + 1 ; i <= n + n ; i++ ) 
	{
		if(par[i] <= n && f[par[i]][i - n]) printf("%d " , par[i]);
		else printf("0 ");
	}
	return 0;
}
/*
*/


免責聲明!

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



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