前言
- 這東西雖然我早就學過了,但是最近才發現我以前學的是假的,心中感慨萬千(霧),故作此篇。
簡介
- 帶權二分圖:每條邊都有權值的二分圖
- 最大權匹配:使所選邊權和最大的匹配
- 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;
}
小結