1. 程式人生 > 其它 >【二分圖】匈牙利 & KM

【二分圖】匈牙利 & KM

【二分圖】匈牙利 & KM

二分圖

概念:

一個圖 \(G=(V,E)\) 是無向圖,如果頂點 \(V\) 可以分成兩個互不相交地子集 \(X,Y\)

且任意一條邊的兩個頂點一個在 \(X\) 中,一個在 \(Y\) 中,則稱 \(G\) 是二分圖


性質:

當且僅當無向圖 \(G\) 的所有環都是偶環時, \(G\) 才是個二分圖


判定:

可從任意一點開始 \(\text{dfs}\),按距離編號

如果要編號的點已經被編號,可根據奇偶判斷是否是二分圖

bool dfs(int u) {
    for (int i = lst[u], v; i; i = nxt[i]) {
        v = to[i];
        if (cl[u] == cl[v])
            return 0;
        if (!cl[v]) {
            cl[v] = 3 - cl[u];
            if (!dfs(v))
                return 0;
        }
    }
    return 1;
}
int main() {
    cl[1] = 1;
    dfs(1);
}

二分圖的匹配

概念:

一個二分圖 \(G\) 的一個子圖 \(M\), 若在 \(M\) 中的任意兩條邊都不依附同一個頂點

則稱 \(M\) 是一個匹配


最大匹配:

即選擇邊數最大的匹配

完備匹配:

某一部的頂點全部與匹配中的某條邊關聯

完美匹配:

所有頂點都和匹配中的某條邊關聯

增廣路

定義:

\(M\) 是二分圖 \(G\) 的已匹配邊的集合,若 \(P\) 是一條聯通兩個在不同部的未匹配點的路徑,

且路徑上匹配邊與未匹配邊交替出現,則 \(P\) 是相對於 \(M\) 的增廣路。


性質:

  • 第一條和最後一條都是未匹配邊,邊數為奇數

    因為要聯通兩個在不同部的未匹配點

  • 一個增廣路徑 \(P\)

    經過取反可以得到一個更大的匹配 \(M'\)

  • \(M\)\(G\) 的最大匹配當且僅當不存在相對於 \(M\) 的增廣路

最大匹配

匈牙利演算法

根據增廣路的性質,我們也可以想到一種演算法

  1. 清空 \(M\)
  2. 尋找 \(M\) 的增廣路 \(P\),通過取反得到更大的 \(M'\) 代替 \(M\)
  3. 重複 (2) 直到找不到增廣路為止

這就是匈牙利演算法

實現

\(\text{dfs}\) ,從 \(X\) 部的一個未匹配點開始,訪問鄰接點 \(v\) (一定是 \(Y\) 部的)

  • 如果 \(v\) 未匹配,則已找到一條增廣路

  • 否則,找出 \(v\) 的匹配頂點 \(w\)

    (一定是 \(X\) 部的),則 \((w,v)\) 是匹配邊

    因為要"取反",所以要使 \((w,v)\) 未匹配, \((u,v)\) 匹配。

    能實現這一點就需要從 \(w\) 出發找一條新增廣路,如果行,則可以找到增廣路

實現:

// cx[i] 是 X 部的點 i 匹配的 Y 部點
// cy[i] 是 Y 部的點 i 匹配的 X 部點
bool dfs(int u) {
	for (int i = 1; i <= m; i++)
		if (mp[u][i] && !vis[i]) {
			vis[i] = 1;
			if (!cy[i] || dfs(cy[i])) {
				cx[u] = i, cy[i] = u;
				return 1;
			}
		}
	return 0;
}
inline void match() {
	memset(cx, 0, sizeof(cx));
	memset(cy, 0, sizeof(cy));
	for (int i = 1; i <= n; i++) {
		memset(vis, 0, sizeof(vis));
		ans += dfs(i);
	}
}

時間複雜度:

  • 鄰接矩陣:\(O(n^3)\)
  • 前向星:\(O(nm)\)

最大匹配的性質

  1. 最小點覆蓋 = 最大匹配

    最小點覆蓋:選擇最少的點使得每條邊都至少和其中一個點關聯

    • 證明:最大匹配能保證剩下的邊都與至少一個點關聯
  2. 最小邊覆蓋 = 總點數 - 最大匹配

    最小邊覆蓋:選擇最少的邊去覆蓋所有點

    • 證明:設總點數是 \(n\),最大匹配是 \(m\)

      則最大匹配能覆蓋 \(2m\) 個點,設剩下 \(a\) 個點

      則這 \(a\) 個點需要單獨用 \(a\) 條邊覆蓋,最小邊覆蓋 = \(m+a\)

      因為 \(2m+a=n\)

      所以最小邊覆蓋 = \((2m+a)-m=n-m\)

  3. 最大點獨立集 = 總點數 - 最小點覆蓋

    最大點獨立集:在二分圖中選出最多的頂點,使得任兩點之間沒有邊相連

    • 證明:刪去最小點覆蓋的點集,對應的邊也沒有了,剩下的點就是獨立集

      因為是最小點覆蓋,減去後就是最大點獨立集

最佳匹配

如果邊有權,則權和最大的匹配叫最佳匹配

有連線 \(X,Y\) 部的頂點 \(X_iY_j\) 的一條邊 \(w_{i,j}\) ,要求一種使 \(\sum w_{i,j}\) 最大的匹配


頂標:給頂點的一個標號

\(X_i\) 的頂標 \(A_i\)\(Y_j\) 的頂標 \(B_j\)則在任意時刻需要滿足 \(A_i+B_j\ge w_{i,j}\) 成立


相等子圖:由 \(A_i+B_j=w_{i,j}\) 的邊構成的字圖

性質:如果相等字圖有完備匹配,那麼這個完備匹配是最佳匹配

KM

核心:通過修改頂標使得能找到完備匹配

  1. 初始化: \(A_i\) 為所有與 \(X_i\) 相連邊的最大權, \(B_i=0\)

  2. 尋找完備匹配失敗,得到一條路徑,叫做交錯樹

    將交錯樹中 \(X\) 部的頂標全部減去 \(d\)\(Y\) 部的頂標全部加上 \(d\) ,會發現

    • 兩端都在交錯樹中的邊, \(A_i+B_j\) 不變,仍在相等字圖中

      都不在,仍然不在相等字圖

    • \(X\) 不在 \(Y\) 在,\(A_i+B_j\) 增大,仍然不在相等字圖

    • \(X\)\(Y\) 不在,\(A_i+B_j\) 減小,現在可能在相等字圖中,使得相等字圖擴大

    為了使至少有一條邊進入相等字圖,且 \(A_i+B_j\ge w_{i,j}\) 恆成立

    \(d\) 應該等於 \(\min A_i+B_j-w_{i,j}\)

  3. 重複 (2) 直到找到完備匹配

複雜度:樸素實現是 \(O(n^4)\)

在相等字圖上找增廣路 \(O(n^2)\) ,每次改頂標最多 \(n\) 次增廣路,要改 \(n\) 次頂標

// lx, ly :頂標
// cy[i] 是 y 部點 i 匹配的 X 部點
bool dfs(int x) {
   vx[x] = 1;
   for (int i = 1, cz; i <= n; i++) {
       if (vy[i]) continue;
       cz = lx[x] + ly[i] - mp[x][i];
       if (cz == 0) {
           vy[i] = 1;
           if (!cy[i] || dfs(cy[i])) {
               cy[i] = x;
               return 1;
           }
       } else lack = min(lack, cz);
   }
   return 0;
}
inline void KM() {
   memset(cy, 0, sizeof(cy));
   for (int i = 1; i <= n; i++)
       for (;;) {
           memset(vx, 0, sizeof(vx));
           memset(vy, 0, sizeof(vy));
           lack = 2e9;
           if (dfs(i)) break;
           for (int j = 1; j <= n; j++) {
               if (vx[j]) lx[j] -= lack;
               if (vy[j]) ly[j] += lack;
           }
       }
}

\(O(n^3)\) 的做法

其實是可以實現 \(O(n^3)\) 的,

  • 我們給每個 \(Y\) 頂點一個鬆弛量 \(\text{slack}\) ,初始為正無窮

  • 對於每條邊 \((i, j)\), 若不在相等字圖中,\(\text{slack}_j=\min(A_i+B_j-w_{i,j})\)

  • 修改頂標時 \(d\) 取所有不在交錯樹中的 \(Y\) 部點\(\text{slack}\) 的最小值

  • 修改完後,所有不在交錯樹中的 \(Y\) 部點\(\text{slack}\) 都減去 \(d\)

int slk[N], pre[N], mat[N];
// mat 等同於 cy
inline void bfs(int st) {
    memset(pre, 0, sizeof(pre));
    for (int i = 1; i <= n; i++) slk[i] = 1e9;
    int x, y = 0, del, pos;
    mat[y] = st;
    do {
        x = mat[y], vy[y] = 1, del = 1e9;
        for (int i = 1; i <= n; i++) {
            if (vy[i]) continue;
            if (slk[i] > lx[x] + ly[i] - mp[x][i])
                slk[i] = lx[x] + ly[i] - mp[x][i], pre[i] = y;
            if (slk[i] < del) del = slk[i], pos = i;
        }
        for (int i = 0 ; i <= n; i++) {
            if (vy[i]) lx[mat[i]] -= del, ly[i] += del;
            else slk[i] -= del;
        }
        y = pos;
    } while (mat[y]);
    while (y) mat[y] = mat[pre[y]], y = pre[y];
}
inline void KM() {
    memset(mat, 0, sizeof(mat));
    memset(lx, 0, sizeof(lx));
    memset(ly, 0, sizeof(ly));
    for (int i = 1; i <= n; i++) {
        memset(vy, 0, sizeof(vy));
        bfs(i);
    }
}

答案

for (int i = 1; i <= n; i++) {
    if (mp[mat[i]][i]==-1e9 || !mat[i]) {
        puts("-1");
        break;
    }
    ans += mp[mat[i]][i];
}

End