二分圖最大權匹配——KM算法


前言

  • 這東西雖然我早就學過了,但是最近才發現我以前學的是假的,心中感慨萬千(霧),故作此篇。

簡介

  • 帶權二分圖:每條邊都有權值的二分圖
  • 最大權匹配:使所選邊權和最大的匹配
  • KM算法,全稱Kuhn-Munkres算法,是用於解決最大權匹配的一種算法。
  • 根據我的理解,該算法算是一種基於貪心的松弛算法,它通過設置頂標將原問題轉化為求一個完備匹配(完備匹配:匹配數=min(左部點數,右部點數))。

流程

  • 設左部中點\(x\)的頂標\(wx_x\)、右部中點\(y\)的頂標\(wy_y\)。初始時\(wx_u=\max\{w_{u,v}\}\)\(wy_v=0\)
  • 我們掃一遍左部,每掃到一個\(x\)點,嘗試增廣,我們只能走滿足條件\(wx_u+wy_v=w_{u,v}\)的邊;這種邊構成了原圖的相等子圖(不要問我為什么,它就叫這個名字)。我們增廣失敗,將訪問過的點(包括增廣失敗的點)形成的樹稱為交錯樹,該樹顯然所有葉子都是\(x\)點。
  • 接下來即是算法關鍵:我們為擴大相等子圖(使當前的\(x\)盡量匹配上),修改所有交錯樹中的點的頂標,即將其中的\(x\)點頂標\(-d\)\(y\)點頂標\(+d\)。為保速度,\(d=\min\{wx_u+wy_v-w_{u,v}\}\)\(u\)在交錯樹中,\(v\)不在交錯樹中)。
  • 由於我們要嘗試為左部\(n\)個點匹配,每次匹配最多增廣\(n\)次(即最多要修改\(n\)次頂標,因為無法保證修改完一次頂標后就能擴大相等子圖),每次增廣是\(O(n+m)\)的,故此做法的復雜度應為\(O(n^2(n+m))\)

某個優化

  • 給每個\(y\)頂點一個“松弛量”函數\(slack\),每次開始找增廣路時初始化為無窮大。在尋找增廣路的過程中,檢查邊<i,j>時,如果它不在相等子圖中,則讓\(slack[j]\)變成原值與\(w_{i,j}\)的較小值。這樣,在修改頂標時,取所有不在交錯樹中的\(y\)頂點的\(slack\)值中的最小值作為\(d\)值即可。但還要注意一點:修改頂標后,要把所有的不在交錯樹中的\(y\)頂點的\(slack\)值都減去\(d\)
  • 這個優化似乎是很有用,但並不能把KM優化到\(O(n^3)\)。這其實和原算法差不多,還是要為左部\(n\)個點匹配,每次匹配還是最多要增廣\(n\)次,每次增廣還是\(O(n+m)\)。如果是完全圖,並且出題人稍微構造一下數據,依然是\(O(n^4)\)

Code

bool dfs(int k) {
	visx[k] = 1;
	F(i, 1, n) {
		if (!visy[i]) {
			int t = A[k] + B[i] - Edge[k][i];
			if (!t) {
				visy[i] = 1;
				if (!link[i] || dfs(link[i])) return link[i] = k;
			} else slack[i] = min(slack[i], t);
		}
	}
	return 0;
}

int KM() {
	mem(link, 0);
	F(i, 1, n) {
		A[i] = -1e18, B[i] = 0;
		F(j, 1, n)
			A[i] = max(A[i], Edge[i][j]);
	}
	F(v, 1, n) {
		int cnt = 0;
		F(i, 1, n) slack[i] = 1e18;
		while (1) {
			mem(visx, 0), mem(visy, 0);
			if (dfs(v)) break;
			int d = 1e18;
			F(i, 1, n) if (!visy[i]) d = min(d, slack[i]);
			F(i, 1, n) if (visx[i]) A[i] -= d;
			F(i, 1, n) if (visy[i]) B[i] += d; else slack[i] -= d;
		}
	}
	Ans = 0;
	F(i, 1, n) Ans += A[i] + B[i];
	return Ans;
}

再次優化

  • 先前的算法中,我們把大量時間浪費在 修改頂標-嘗試增廣 的操作上了。每次修改完頂標后,我們都要花至多\(O(n^2)\)的時間走先前已經走過的路。
  • 但實際上,每次修改頂標后,我們可以確定一個\(y\)點可以被增廣:那就是迫使我們修改頂標的那個\(y\)點。我們可以記錄下它,並且下次增廣就直接從它已連的\(x\)點增廣(當然,如果它沒有連\(x\)點,那就增廣結束)。
  • 這樣,我們就把\(dfs\)的增廣改為了一個類似\(bfs\)的東西。並且對於每個\(x\)點而言,每次修改頂標后不需要清空\(vis\)數組、增廣時每個點、每條邊至多被經過一次,故時間復雜度成功優化至\(O(n^2+nm)\)

Code

int n,w[N][N],my[N],wx[N],wy[N],slack[N],pre[N];
bool vis[N];
void augment(int s)
{
	fo(y,1,n) vis[y]=uy[y],slack[y]=inf;
	int y0,nxt=0,tm;
	for(my[0]=s; vis[y0=nxt]=1,my[y0];)
	{
		int x=my[y0],d=inf;
		fo(y,1,n) if(!vis[y])
		{
			if((tm=wx[x]+wy[y]-w[x][y])<slack[y]) slack[y]=tm,pre[y]=y0;
			if(slack[y]<d) d=slack[y],nxt=y;
		}
		if(d<inf) fo(y,0,n) vis[y]?wx[my[y]]-=d,(y?wy[y]+=d:0):slack[y]-=d;
	}
	for(int y; y0; y0=pre[y=y0],my[y]=my[y0]);
}
int KM()
{
	fo(i,1,n) wx[i]=wy[i]=my[i]=pre[i]=0;
	fo(i,1,n) fo(j,1,n)
	{
		if(wx[i]<w[i][j]) wx[i]=w[i][j];
  		if(wy[j]<w[i][j]) wy[j]=w[i][j];
	}
  	fo(i,1,n) augment(i);
	int s=0;
	fo(i,1,n) s+=wx[i]+wy[i];
	return s;
}

小結

  • 學算法時要帶着腦子,莫被網上的博客騙了。


免責聲明!

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



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