匈牙利演算法又稱 Kuhn-Munkres 演算法 (簡稱 KM) 是用於尋找二分圖最大權匹配。在打競程時,不常遇到這類題目,就算遇到也算是很好發揮,把問題轉化再套模板即可。然而,其原理卻相當複雜,理解原理後也有利於理解如何轉化匹配類型的題目,因此這篇會把整套原理說明清楚
閱讀之前可以先看前篇三篇匹配與霍爾定理、Kőnig 定理、擴充路徑演算法,有利於了掌握匹配問題的脈絡
有一個農業公司擁有
我們可以把他們的關係化成二分圖,田地
承上述情境,有一天政府認為玉米產量過剩,因此希望可以付錢使停止生產與製造。若公司同意不繼續在第
我們稱這問題是一個最小權點覆蓋問題
一組權重覆蓋,是一組標記 (label) 的選擇,分別是
在下圖中,可以看到一張圖有邊權與點權,點權的加總是
若把
當一張圖並沒有點權時,我們可以自由設計點權,使對於所有
Egerváry 定理是 Kőnig 定理的拓展。簡單來說,Jenő Egerváry 引入了權重的概念到 Kőnig 定理當中,因此這兩定理又被稱為 Kőnig-Egerváry 定理
Egerváry 定理必須符合三個條件:
對於一張帶邊權的二分圖,若其邊權不為負,且存在一個匹配
換句話說,有一帶權點覆蓋
一張有權二分圖的等效子圖
右圖為左圖的等效子圖
有了以上材料,我們可以來說明匈牙利演算法到底如何運行
根據 Egerváry 定理的描述,其實整個演算法要達到的目標僅有兩個:
要達成這些目標,我們需要不斷的調整權重,直到
先定義一個名詞 excess,是指對於
令
找出等效子圖
其中,尋找最大匹配
註 : 這邊以下所使用的
首先,將
我們可以觀察下圖其中一個節點
接下來下一個步驟,我們需要去找出等效子圖
我們可以找出大小為
可以注意的是,此時的
我們可以再找出大小為
可以注意到,此時
實作主要可以分為原始的
然後又依走訪方法不同分為 DFS 與 BFS 兩種版本
我兩種都放,這樣比較有趣一點
這版本是原本尚未經過優化的版本
在實作上,我們可以定義一個
需要注意的是,在運算時,
int g[MAXN][MAXN], lx[MAXN], ly[MAXN]; // g: 鄰接矩陣, lx, ly: 標記值
int my[MAXN], sy[MAXN], n; // my: y 的配對點, sy: slack 優化
bool vx[MAXN], vy[MAXN]; // 紀錄 x, y 是否拜訪過
bool dfs(int x) { // 擴充路徑演算法
if (vx[x])
return false;
vx[x] = true;
for (int y = 0, excess; y < n; ++y) {
if (vy[y])
continue;
excess = lx[x] + ly[y] - g[x][y];
if (!excess) { // excess = 0, 代表找到了匹配邊
vy[y] = true;
if (my[y] == -1 || dfs(my[y])) {
my[y] = x;
return 1;
}
}
else if (sy[y] > excess)
sy[y] = excess; // 找最小 excess
}
return false;
}
int hungarian() {
memset(ly, 0, sizeof(int) * n);
memset(my, -1, sizeof(int) * n);
for (int x = 0; x < n; ++x) {
lx[x] = -INF;
for (int y = 0; y < n; ++y)
lx[x] = max(lx[x], g[x][y]); // X 的標記值取每 row 的最大值
}
for (int x = 0; x < n; x++) {
for (int y = 0; y < n; y++) // 記得清成 INF
sy[y] = INF;
while (true) {
memset(vx, 0, sizeof(bool) * n); // 先將拜訪清單清空
memset(vy, 0, sizeof(bool) * n);
if (dfs(x)) // 若找到新的擴充路徑就離開
break;
// 找出在 X-R 與 Y-T 之間連邊的值中
// 最小的當作 epsilon
int epsilon = INF;
for (int y = 0; y < n; ++y) {
if (!vy[y])
epsilon = min(epsilon, sy[y]);
}
// 修改 x, y 標記的值 u, v
for (int j = 0; j < n; ++j) {
if (vx[j]) // X - R
lx[j] -= epsilon;
if (vy[j]) // T
ly[j] += epsilon;
else // Y - T
sy[j] -= epsilon;
}
}
}
// 算答案
int ans = 0;
for (int y = 0; y < n; ++y)
ans += g[my[y]][y];
return ans;
}
總時間複雜度:
位於修改
// 變數大概都和上面差不多
typedef long long ll;
int n, mx[MXN], my[MXN], pa[MXN]; // pa: 維護路徑的東西
ll g[MXN][MXN], lx[MXN], ly[MXN], sy[MXN];
bool vx[MXN], vy[MXN];
void init(int _n) { // 初始化
n = _n;
for (int i = 1; i <= n; i++) // 1-based
fill(g[i], g[i] + n + 1, 0);
}
void addEdge(int x, int y, ll w) { // 加邊
g[x][y] = w;
}
void augment(int y) { // 把擴充路徑都反轉
for (int x, z; y; y = z)
x = pa[y], z = mx[x], my[y] = x, mx[x] = y;
}
void bfs(int st) {
for (int i = 1; i <= n; ++i)
sy[i] = INF, vx[i] = vy[i] = false;
queue<int> q;
q.push(st);
while (true) {
while (q.size()) { // 擴充路徑演算法
int x = q.front();
q.pop();
vx[x] = true;
for (int y = 1; y <= n; ++y)
if (!vy[y]) {
ll excess = lx[x] + ly[y] - g[x][y];
if (!excess) {
pa[y] = x;
if (!my[y]) { // 若沒有匹配就代表有擴充路徑
augment(y); // 所以就反轉
return;
}
vy[y] = true, q.push(my[y]);
}
else if (sy[y] > excess)
pa[y] = x, sy[y] = excess;
}
}
// 找出在 X-R 與 Y-T 之間連邊的值中
// 最小的當作 epsilon
ll epsilon = INF;
for (int y = 1; y <= n; ++y)
if (!vy[y] && epsilon > sy[y])
epsilon = sy[y];
// 修改 x, y 標記的值 u, v
for (int j = 1; j <= n; ++j) {
if (vx[j])
lx[j] -= epsilon;
if (vy[j])
ly[j] += epsilon;
else
sy[j] -= epsilon;
}
// 這裡就是他優化奇妙的地方
for (int y = 1; y <= n; ++y) {
if (!vy[y] && sy[y] == 0) {
if (!my[y]) {
augment(y);
return;
}
// 在這裡加進去後只會跑一次反轉路徑
vy[y] = true, q.push(my[y]);
}
}
}
}
ll hungarian() {
fill(mx, mx + n + 1, 0);
fill(my, my + n + 1, 0);
fill(ly, ly + n + 1, 0);
fill(lx, lx + n + 1, -INF);
for (int x = 1; x <= n; ++x)
for (int y = 1; y <= n; ++y)
lx[x] = max(lx[x], g[x][y]);
for (int x = 1; x <= n; ++x)
bfs(x);
ll ans = 0;
for (int y = 1; y <= n; ++y)
ans += g[my[y]][y]; // 答案存在 my[]
return ans;
}
總時間複雜度:
因為用圖來看實在太麻煩了,所以我們可以用
註 : 因為中文太複雜了我總是分不清列與行,因此我就用 row 與 column 來說明,以防自己寫錯
如上面例子,我們再來處理一次這張二分圖,但這次使用矩陣來處理
如上圖,我們可以得到以下鄰接矩陣,裡面每一個元素都表示邊權
我們可以定義一個 excess 矩陣
如果將 excess 矩陣對照等效子圖
畫直線與橫線最少的線條覆蓋所有的
此時
接下來,藉由直接對照
注意此時的
接下來,對
注意此時的
到此會發現,已經找到一組
註 : 如果在網路上搜尋,會發現另一種矩陣的執行方法。另一種方法是先將每 row 的最小值扣除,再將沒有
題目來源: Codeforces 1107 F. Vasya and Endless Credits
這題根本閱讀題
有人想要不勞而獲買一台車,已知銀行有
每一個方案只能使用一次
每個月最多只能最多申請一個方案
根據題目,最多只會有
/* 以上略 */
int main(){
ll n, a, b, k;
cin >> n;
init(n);
for(int i = 1; i <= n; i++){
cin >> a >> b >> k;
for(int j = 0; j < n; j++)
addEdge(i, j + 1, max(0LL, a - min((ll)j, k) * b));
}
cout << hungarian() << '\n';
return 0;
}
加負號在每一條邊的邊權,即可反轉大小關係。雖然這樣看似違反 Egerváry 定理的限制,但其實很多人寫的程式實作不會因此受影響。如果會影響的話,就抓個最大值來減去所有邊權,這樣依樣可以轉化成可以解的問題
在大小為
匈牙利演算法是由美國數學家 Harold Kuhn 在 1955 年提出 (參見原文: The Hungarian method for the assignment problem),會取名「匈牙利」是為了紀念兩位匈牙利數學家 Dénes Kőnig 與 Jenő Egerváry,也就是提出 Kőnig-Egerváry 定理的那兩位數學家,因為這個演算法有很多內容都是基於那兩位匈牙利數學家的貢獻
其中,Dénes Kőnig 曾寫出第一本圖論的書,英譯為 Theory of finite and infinite graphs (書裡面很多表示方法都不是現代圖論會用的,而且圖偏少,沒事不要讀),可說是現代圖論領域的開山祖之一
在 1957 年數學家 James Munkres (就是寫出那本很經典的 Topology 的那位),證出匈牙利演算法是
從某些角度看來,擴充路徑演算法只是匈牙利演算法的子程式,有許多資料 (中文居多) 會錯誤地將擴充路徑演算法講成匈牙利演算法
OnlineJudge 11383 - Golden Tiger Claw (匈牙利演算法觀念裸題)
OnlineJudge 1045 - The Great Wall Game
OnlineJudge 10746 - Crime Wave - The Sequel
OnlineJudge 10888 - Warehouse
ShanC 程式競賽筆記
作者: ShanC
更新: 2025/2/5