1. 程式人生 > 其它 >網路流與二分圖

網路流與二分圖

網路流與二分圖

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)\)

。對於任意一個可行流,我們知道它的流量就是它任意一種割的所有割邊的流量之和(感性理解),由於容量限制,它顯然不大於割的容量,因此任何一個流的流量不大於任何一個割的容量。而最大流顯然滿足殘量網路上 \(S,T\)​ 不連通,因此容易找到這樣一種割,使得割邊流量之和等於割的容量,即最大流 = 最小割。

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. 二項式反演。