網路流與二分圖
0. Change log
2021.12.5:更換模板程式碼。新增二分圖部分。
1. 網路流
網路流的關鍵在於建模,建模是精髓,建模是人類智慧。
1.1. 網路最大流
Maximum Flow,簡稱 MF。
一個有向圖網路 \(G=(V,E)\),給出源匯點 \(S,T\),每條邊 \((u,v)\) 有容量 \(c(u,v)\)。特別的,若 \((u,v) \notin E\),則 \(c(u,v)=0\)。求 \(S\to T\) 最多能流多少流量。
- 流函式:設 \(f(x,y)\) 是一個 \((x,y)\to\mathbb R\) 的二元函式,其中 \(x,y\in V\)
- 容量限制:每條邊的流量不超過容量,即 \(f(x,y)\leq c(x,y)\)。
- 斜對稱:\(f(x,y)=-f(y,x)\)。
- 流量守恆:除源匯點以外,從每個節點流入和流出的流量相等,即 \(\forall x\neq S,x\neq T\),\(\sum_{(u,x)\in E}f(u,x)=\sum_{(x,v)\in E}f(x,v)\)。
- 殘量網路:所有 \(f(x,y)\neq c(x,y)\) 的 \((x,y)\) 組成的有向圖。
- 增廣路:一條 \(S\to T\) 的路徑,滿足路徑上的所有邊都在殘量網路中。
演算法的核心思想是在每次增廣時,都給當前邊的反邊的容量
技巧:網路流建圖一般使用鏈式前向星,將每條邊與它的反向邊連續儲存,編號分別記為 \(i\) 和 \(i+1\),\(i\) 是偶數。可以快速求得 \(i\) 的反向邊編號為 \(i\oplus 1\),很方便。因此初始 \(cnt\) 應設為 \(1\)。
1.1.1. 最大流 = 最小割
將 \(V\) 分成互不相交的兩個點集 \(A,B\),其中 \(S\in A\),\(T\in B\),這種點的劃分方式叫做割。定義割的容量為 \(\sum_{x\in A,y\in B}c(x,y)\)
1.1.2. EK 演算法
Edmonds - Karp,簡稱 EK。
核心思想:不斷找長度最小的增廣路進行增廣,BFS 實現。需要記錄流向每個點的邊的編號,然後從 \(T\) 不斷反推到 \(S\)。時間複雜度 \(\mathcal{O}(nm^2)\)。
int cnt = 1, hd[N], to[M << 1], nxt[M << 1], lim[M << 1];
void add(int u, int v, int w) {
nxt[++cnt] = hd[u], hd[u] = cnt, to[cnt] = v, lim[cnt] = w;
nxt[++cnt] = hd[v], hd[v] = cnt, to[cnt] = u, lim[cnt] = 0;
} int n, m, s, t, fr[N], vis[N], fl[N]; ll ans;
int main(){
cin >> n >> m >> s >> t;
for(int i = 1, u, v, w; i <= m; i++) cin >> u >> v >> w, add(u, v, w);
while(1) {
queue <int> q; mem(fl, 0, N), mem(vis, 0, N);
fl[s] = inf, vis[s] = 1, q.push(s);
while(!q.empty()) {
int t = q.front(); q.pop();
for(int i = hd[t]; i; i = nxt[i]) {
int it = to[i];
if(!lim[i] || vis[it]) continue;
vis[it] = 1, fl[it] = min(fl[t], lim[i]);
fr[it] = i ^ 1, q.push(it);
}
} if(!fl[t]) break;
int p = t; ans += fl[t];
while(p != s) lim[fr[p]] += fl[t], lim[fr[p] ^ 1] -= fl[t], p = to[fr[p]];
} cout << ans << endl;
}
1.1.3. Dinic 演算法
核心思想:BFS 分層找 \(dis_x\) 表示從 \(S\) 到 \(x\) 至少要經過殘量網路上的 \(dis_x\) 條邊,眾所周知這是有向無環分層圖。僅在兩層 \(dis_y=dis_x+1\) 之間增廣。因為是 DAG 所以可 DFS 多路增廣。
當前弧優化:增廣時容量等於流量的邊無用,可以直接跳過。記錄從每個點出發第一條沒有流滿的邊,稱為當前弧。每次 DFS 到該點就從當前弧開始增廣。注意,每次多路增廣前當前弧應初始化設為鏈式前向星的頭,因為並不是一旦流量等於容量,這條邊就永遠無用:反向邊流量的增加會讓它重新出現在殘量網路中。當前弧優化的 Dinic 時間複雜度 \(\mathcal{O}(n^2m)\),不加會退化至 \(\mathcal{O}(nm^2)\)。
關於當前弧優化的注意事項:
for(int i = cur[u]; res && i; i = nxt[i]) {
cur[u] = i;
// ......
}
上述程式碼不可以寫成:
for(int &i = cur[u]; res && i; i = nxt[i]) {
// ......
}
因為如果 \(u\to v\) 這條邊讓剩餘流量 res
變成 \(0\),第二種寫法會直接跳過 \((u,v)\),但實際上 \((u,v)\) 並不一定流滿,所以不應跳過。這會導致當前弧跳過很多不應該跳的邊,使增廣效率降低,從而大幅降低程式執行效率(實際表現比 EK 還要差)。
另一種解決方法是在迴圈末尾判斷 if(!res) return flow;
。總之,在寫當前弧優化時千萬注意不能跳過沒有流滿的邊。模板題 P3381 【模板】最小費用最大流 程式碼如下:
int cnt = 1, hd[N], to[M << 1], nxt[M << 1], lim[M << 1];
void add(int u, int v, int w) {
nxt[++cnt] = hd[u], hd[u] = cnt, to[cnt] = v, lim[cnt] = w;
nxt[++cnt] = hd[v], hd[v] = cnt, to[cnt] = u, lim[cnt] = 0;
} int n, m, s, t, dis[N], cur[N]; ll ans;
ll dfs(int u, ll res) {
if(u == t || !res) return res; ll flow = 0;
for(int i = cur[u]; res && i; i = nxt[i]) {
int it = to[i], c = min(res, (ll)lim[i]); cur[u] = i;
if(c && dis[u] + 1 == dis[it]) {
ll k = dfs(it, c);
flow += k, res -= k, lim[i] -= k, lim[i ^ 1] += k;
}
} return dis[u] = flow ? dis[u] : 0, flow;
}
int main(){
cin >> n >> m >> s >> t;
for(int i = 1, u, v, w; i <= m; i++) cin >> u >> v >> w, add(u, v, w);
while(1) {
queue <int> q; mem(dis, 0x3f, N), dis[s] = 0, q.push(s);
while(!q.empty()) {
int t = q.front(); q.pop();
for(int i = hd[t]; i; i = nxt[i])
if(lim[i] && dis[to[i]] > 1e9)
dis[to[i]] = dis[t] + 1, q.push(to[i]);
} if(dis[t] > 1e9) break; cpy(cur, hd, N), ans += dfs(s, 1e18);
} cout << ans << endl;
}
1.1.4. ISAP
1.1.5. HLPP
1.2. 最小費用最大流
Minimum cost maximum flow,簡稱 MCMF。相較於一般網路最大流,在原有網路的基礎上,每條邊多了一個權值 \(w(x,y)\)。在保證最大流的情況下,需要求出 \(\sum_{(x,y)\in E}f(x,y)w(x,y)\) 的最小值。
1.2.1. 無負環的 SSP 演算法
Successive Shortest Path,簡稱 SSP。
核心思想:每次找到長度最短的增廣路進行增廣。
- EK:將 BFS 換成 SPFA 即可。
- Dinic:將 BFS 換成 SPFA,多路增廣時僅在 \(dis_x+w(x,y)=dis_y\) 的 \(x,y\) 之間增廣。
時間複雜度 \(\mathcal{O}(nmf)\),其中 \(f\) 為最大流流量。實際應用中此上界非常鬆,因為不僅增廣次數遠遠達不到 \(f\),同時 SPFA 的複雜度也遠遠達不到 \(nm\)。正確性證明見 OI-Wiki。
1.2.2. 無負環的 Primal-Dual 原始對偶演算法
建議先學習 Johnson 全源最短路演算法。
具體地,我們為每個點賦一個 “勢” \(h_i\),讓原圖的最短路不變。使用 Johnson 全源最短路演算法的思想,先用 SPFA 求出源點到每個點的最短路 \(h_i\),那麼 \(i\to j\) 的新邊權就是 \(w_{i,j}+h_i-h_j\)。正確性證明略。
找到增廣路後,每次增廣都會改變圖的形態,我們只需要用每次增廣時跑出來的最短路加在 \(h\) 上即可,即 \(h_i\gets h_i+dis_i\)。原因如下:
- 如果 \(i\to j\) 在增廣路上,有 \(dis_i+w_{i,j}+(h_i-h_j)=dis_j\)。即 \(w_{j,i}+(dis_j+h_j)-(dis_i+h_i)=0\)(\(w_{i,j}=-w_{j,i}\)),反邊邊權為 \(0\)。
- 對於原有的邊,我們有 \(dis_i+w_{i,j}+(h_i-h_j)\geq dis_j\),即 \(w_{i,j}+(dis_i+h_i)-(dis_j+h_j)\geq 0\),邊權仍然非負。
經過上述轉化,我們可以使用 Dijkstra 而不是 SPFA 求解增廣路。實際應用中相較於 SSP 並沒有很大的優勢。費用流模板題程式碼:
const int N = 5e3 + 5;
const int M = 5e4 + 5;
int cnt = 1, hd[N], to[M << 1], nxt[M << 1], lim[M << 1], cst[M << 1];
void add(int u, int v, int w, int c) {
nxt[++cnt] = hd[u], hd[u] = cnt, to[cnt] = v, lim[cnt] = w, cst[cnt] = c;
nxt[++cnt] = hd[v], hd[v] = cnt, to[cnt] = u, lim[cnt] = 0, cst[cnt] = -c;
} int n, m, s, t, h[N], vis[N], dis[N], fr[N], flow, cost;
int main(){
cin >> n >> m >> s >> t;
for(int i = 1, u, v, w, c; i <= m; i++) cin >> u >> v >> w >> c, add(u, v, w, c);
queue <int> q; mem(h, 0x3f, N), h[s] = 0, q.push(s);
while(!q.empty()) {
int t = q.front(); q.pop(); vis[t] = 0;
for(int i = hd[t]; i; i = nxt[i]) if(lim[i]) {
int it = to[i], d = h[t] + cst[i];
if(d < h[it]) {h[it] = d; if(!vis[it]) vis[it] = 1, q.push(it);}
}
} while(1) {
priority_queue <pii, vector <pii>, greater <pii>> q;
mem(dis, 0x3f, N), mem(vis, 0, N), q.push({dis[s] = 0, s});
while(!q.empty()) {
pii t = q.top(); q.pop();
if(vis[t.se]) continue; vis[t.se] = 1;
for(int i = hd[t.se]; i; i = nxt[i]) if(lim[i]) {
int it = to[i], d = t.fi + cst[i] + h[t.se] - h[it];
if(d < dis[it]) fr[it] = i, q.push({dis[it] = d, it});
}
} if(dis[t] > 1e9) break;
int c = (1ll << 31) - 1;
for(int i = 1; i <= n; i++) h[i] += dis[i];
for(int i = t; i != s; i = to[fr[i] ^ 1]) cmin(c, lim[fr[i]]);
for(int i = t; i != s; i = to[fr[i] ^ 1]) lim[fr[i]] -= c, lim[fr[i] ^ 1] += c;
flow += c, cost += h[t] * c;
} cout << flow << " " << cost << "\n";
}
1.3. 例題
I. P4249 [WC2007]剪刀石頭布
現在你已經對網路流的基本原理有了一定了解,就讓我們來看一看下面這個簡單的例子,把我們剛剛學到的知識運用到實踐中吧。
注意到對於任意三個不相同的點 \(i,j,k\),若它們不能構成三元環,則一定有且只有一個點選敗了另外兩個點。列舉這個點 \(i\),那麼對於它所有出點中任意兩個 \(j,k\) 都無法組成三元環。因此最終答案為 \(\dbinom n 3 - \sum_{\\i = 1} ^ n \dbinom {deg_i} 2\)。故我們需要最小化 \(\sum_{\\i = 1} ^ n \dbinom {deg_i} 2\)。
本題核心 trick:拆組合數貢獻。將 \(\dbinom x 2\) 寫作 \(1+2+\cdots+x\),這啟發我們使用網路流:把未被定向的邊 \((u,v)\) 抽象成一個節點 \(c\)。\(S\to c\),\(c\to u\),\(c\to v\) 都連一條容量為 \(1\),代價為 \(0\) 的邊,表示有且僅有一個點被選擇。此外,從每個點 \(u\) 向 \(T\) 連若干條容量為 \(1\),代價分別為 \(deg_u,deg_u+1,\cdots,n-1\),其中 \(deg_u\) 是已經確定的邊中 \(u\) 的出度,然後跑 MCMF 即可。
2. 二分圖
2.1. 定義,判定與性質
二分圖的定義:設無向圖 \(G=(V,E)\),若能夠將 \(V\) 分成兩個點集 \(V_1,V_2\) 滿足:\(V_1\cap V_2=\varnothing\),\(V_1\cup V_2=V\) 且 \(\forall (u,v)\in E\) 都滿足 \(u,v\) 不同屬於 \(V_1\) 或 \(V_2\),那麼稱 \(G\) 是一張二分圖。
一張圖是二分圖的充分必要條件是不存在奇環。必要性:從任意一個點出發,必須經過偶數條邊才能回到這個點;充分性:對不存在奇環的圖黑白染色可以得到一組劃分 \(V_1,V_2\) 的方案。
通過這一性質,我們得到線上性時間 \(|V|+|E|\) 內快速判定二分圖的方法:從任意沒有被染色的節點對其所在連通塊進行黑白染色,若存在一條邊使得其兩端點顏色相同,則存在奇環,不是二分圖。反之則是二分圖。
2.2. 最大匹配
二分圖的匹配的定義如下:給定一張二分圖 \(G=(V,E)\),若其邊匯出子圖 \(G’=(V',E')\subseteq G\) 滿足對於 \(E'\) 中任意兩條邊不交於同一端點,那麼稱 \(G'\) 是二分圖 \(G\) 的一組匹配。
- 邊匯出子圖:選出若干條邊,同這些邊所連線的所有頂點組成的圖。
- 點匯出子圖:選出若干個點,同兩端都在該點集的所有邊組成的圖。
將 \(V\) 劃分成互不相交且內部沒有邊的兩個點集 \(V_1,V_2\)。對於任意節點,最多隻能有一條邊 \((u,v)\in E'\) 與其相連,也就是限制了度數 \(\leq 1\)。
2.2.1. 匈牙利演算法
2.2.2. 網路流
嘗試用網路流解決該問題:從源點 \(S\) 向 \(V_1\) 每個節點連邊,從 \(V_2\) 每個節點向匯點 \(T\) 連邊,再加上原圖的邊。所有邊容量設為 \(1\),\(S\to T\) 的最大流即最大匹配。正確性不難證明:一組可行流與一組匹配一一對應,且流量等於匹配數量。
使用 Dinic 求網路最大流,時間複雜度是神奇的 \(\mathcal{O}(E\sqrt {V})\),證明見 OI-Wiki。模板題 P3386 【模板】二分圖最大匹配 程式碼如下。
const int N = 1e3 + 5;
const int M = 1e5 + 5;
int cnt = 1, hd[N], to[M << 1], nxt[M << 1], lim[M << 1];
void add(int u, int v) {
nxt[++cnt] = hd[u], hd[u] = cnt, to[cnt] = v, lim[cnt] = 1;
nxt[++cnt] = hd[v], hd[v] = cnt, to[cnt] = u, lim[cnt] = 0;
} int n, m, e, t, ans, dis[N], cur[N];
int dfs(int id, int res) {
if(id == t || !res) return res;
int flow = 0;
for(int i = cur[id]; i && res; i = nxt[i]) {
int it = to[i], c = min(lim[i], res); cur[id] = i;
if(c && dis[id] + 1 == dis[it]) {
int k = dfs(it, c);
res -= k, flow += k, lim[i] -= k, lim[i ^ 1] += k;
}
} return dis[id] = flow ? dis[id] : -1, flow;
}
int main(){
cin >> n >> m >> e, t = n + m + 1;
for(int i = 1, u, v; i <= e; i++) cin >> u >> v, add(u, v + n);
for(int i = 1; i <= n; i++) add(0, i);
for(int i = 1; i <= m; i++) add(n + i, t);
while(1) {
queue <int> q; mem(dis, 0x3f, N), dis[0] = 0, q.push(0);
while(!q.empty()) {
int t = q.front(); q.pop();
for(int i = hd[t]; i; i = nxt[i]) if(lim[i]) {
int it = to[i], d = dis[t] + 1;
if(d < dis[it]) dis[it] = d, q.push(it);
}
} if(dis[t] > t) break; cpy(cur, hd, N), ans += dfs(0, N);
} cout << ans << endl;
}
2.3. 最小點覆蓋集
二分圖點覆蓋集定義如下:給定一張二分圖 \(G=(V,E)\),若點集 \(V'\subseteq V\) 滿足對於任意 \((u,v)\in E\) 都有 \(u\in V'\) 或 \(v\in V'\),那麼稱 \(V'\) 是二分圖 \(G\) 的一個覆蓋集。i.e. 一個點可以覆蓋以該點為端點的邊,求覆蓋所有邊的最小點集。
一個二分圖的點覆蓋集與一組割相對應:建出我們求最大匹配時的圖,我們欽定割僅在兩端取到。若在兩部點之間的邊取到,可以調整至兩端,即割 \((u,v)\) 等價於割掉 \((S,u)\) 或 \((v,T)\),因為若 \(u,v\) 之間有流量,那麼 \(S\to u\to v\to T\) 顯然滿流。對於左部點 \(u\in V_1\),如果 \(S\to u\) 被割掉,那麼 \(u\) 屬於最小點覆蓋集。類似地,若右部點 \(v\in V_2\) 滿足 \(v\to T\) 被割掉,\(v\) 也屬於最小點覆蓋集。
上述構造是一組合法的點覆蓋集,反證法可證:若存在邊 \((u,v)\in E\) 沒有被覆蓋,這說明 \(S\to u\to v\to E\) 存在增廣路,與割的定義矛盾。綜上,最小點覆蓋集等於最小割,即最大流,也即二分圖最大匹配。
2.4. 最大獨立集
二分圖的獨立集定義如下:給定一張二分圖 \(G=(V,E)\),若點集 \(V’\subseteq V\) 滿足對於點集中任意兩點不存在連邊,則稱 \(V’\) 是二分圖 \(G\) 的一個獨立集。
考慮二分圖 \(G=(V,E)\) 的最小點覆蓋集 \(V’\)。因為每一條邊都被至少一個 \(u\in V'\) 所覆蓋,所以 \(V\backslash V'\) 的所有點之間互不相連。實際上,獨立集與點覆蓋集一一對應,且獨立集與點覆蓋集交為空,併為 \(V\),即點覆蓋集與獨立集互補。因此二分圖最大獨立集等於 \(|V|\) 減去最小點覆蓋集。
2.5. 最大團
二分圖的團定義如下:給定一張二分圖 \(G=(V,E)\),若其點匯出子圖 \(G'=(V',E')\) 滿足將 \(V'\) 分成互補點集 \(V_1,V_2\) 之後,對於任意 \(u\in V_1,v\in V_2\) 都有 \((u,v)\in E'\),那麼稱 \(G'\) 是二分圖 \(G\) 的一個團。
建出 \(G\) 的補圖 \(G'=(V',E')\),若 \((u,v)\in E'\) 那麼 \(u,v\) 不能同時出現在最大團中。故二分圖最大團等於補圖最大獨立集。
使 \(V_1\) 或 \(V_2\) 儘可能大的最大團演算法:最大團 \(\sim\) 補圖最大獨立集 \(\sim\) 補圖最小點覆蓋集的補集。不妨設我們要使最大團中的 \(|V_1|\) 儘可能大,對應就是使補圖最小點覆蓋的 \(|V_1|\) 儘可能小。根據結論 “一個二分圖的點覆蓋集與一組割相對應”(2.3 有證明),問題等價於求原二分圖 \(G\) 的補圖 \(G’=(V',E')\) 的最小割,使得 \(V_1\in V'\) 中被割掉的點儘量少。
如果僅僅在原圖上面跑最大流,我們無法控制二分圖某部點被割掉的點的數量,考慮如何在不影響最大匹配的前提下為每條邊附上權值,使得我們求出的最小割儘量割 \(v\in V_2\) 與 \(T\) 之間的邊。
不妨將 \(S\) 與 \(u\in V_1\) 之間的邊附上權值 \(c+1\),\(v\in V_2\) 與 \(T\) 之間的邊附上權值 \(c\),這樣可以優先割 \((v,T)\) 讓 \(V_1\) 被割的點儘可能少。但為了保證最大匹配的正確性,\(c\) 應當不小於 \(n=\min(|V_1|,|V_2|)\):不能出現 \(x\) 個左部點匹配了 \(y>x\) 個右部點的情況,即 \((n-1)\times (c+1)<nc\),化簡得到 \(c>n-1\)。不要忘記將 \((u,v)\in E\) 之間的邊權附上 \(n\):因為 \((v,T)\) 的容量限制為 \(n\),\((u,v)\) 的流量不可能超過 \(n\),所以附成 \(n\) 即可。
2.6. 例題
*I. [BZOJ2162]男生女生
本題可以看做割裂的兩部分,一部分是求二分圖的最大團,另一部分則需要用到二項式反演,見 反演與狄利克雷卷積 Part 2. 二項式反演。