網路流

dingzibo_qwq發表於2024-05-05

1 網路流相關概念

網路流的概念分為網路和流。

網路是指一種特殊的有向圖 \(G=(V,E)\),有容量和源匯點 \(s,t\)

對於一個網路,流需要滿足以下性質:

  • 每條邊上的流量不能大於它的容量。
  • 每個點流入的流量等於流出的流量。

而對於整個網路和它上面的流,定義流的流量為源點流出的流量之和。根據第二條性質,它也等於匯點流入的流量。

網路流有很多問題和模型,下面詳細講解。

2 最大流

2.1 問題概述

對於一個網路,找到一個流,使得流的流量最大。

通常情況下,我們使用 Dinic 演算法求解最大流。在此之前需要先了解 FF 增廣。

2.2 FF 演算法

即 Ford-Fulkerson 演算法,是一種計算最大流的演算法的總稱,基於貪心思想。

首先我們對於一個網路 \(G\) 和一個流 \(f\) 給出如下定義:

  • 對於一條邊 \((u,v)\),我們將其容量與流量之差成為剩餘容量。
  • \(G\) 中所有剩餘容量大於 \(0\) 的邊和節點構成的子圖稱為殘量網路,記作 \(G_f\)

我們將 \(G_f\) 上一條從源點到匯點的路徑稱作增廣路,對於任意一條增廣路,我們給每一條邊都加上一個相等的流量,讓整個網路的流量增加,這一過程叫做增廣。

顯然,我們可以將求解最大流的過程看做不斷增廣。因此 FF 的本質就是不斷找增廣路進行增廣,直到找不到為止。

此時考慮這樣的情況:

v2-e3aed80f1eab30d25ec9babfb897d68a_720w.webp (720×619) (zhimg.com)

假如我們此時找到的增廣路為 \(1\to 2\to 3\to 4\),那麼殘量網路就會變成這樣:

v2-c66b961adf5b5dcbf5bf52a91ca74c03_720w.webp (720×619) (zhimg.com)

此時已經不存在增廣路了,然而最大流是 \(1\) 嗎?其實不然,顯然走 \(1\to 3\to 4,1\to 2\to 4\) 最大,流量為 \(2\)

為了解決這樣的問題,我們引入反向邊。我們約定 \(f(u,v)=-f(v,u)\),即反向邊的流量是正向邊流量的相反數。為了保證這個性質,我們在增加 \(f(u,v)\) 值的時候,也要將 \(f(v,u)\) 減少。

可能我們會覺得負數的流量很詭異,不過我們的重點並不在於流量本身,而是殘量網路。當正向邊流量增加時,剩餘容量減少;同時反向邊流量減少,剩餘容量增加。

例如下圖:

v2-1c4016f73a2e94109fbb8769a1e88566_720w.webp (720×619) (zhimg.com)

我們在反方向建邊權為 \(0\) 的邊,此時我們再次找到 \(1\to2\to3\to4\) 這條增廣路,正向的殘量網路容量應該減一,而反向的殘量網路容量要加一,如下圖:

v2-f19ff404a0932ca1d6bdf713403142cf_720w.webp (720×619) (zhimg.com)

這時,我們還可以再找到一條增廣路,即 \(1\to3\to2\to4\)

v2-b79f3fc5c921de2388012808f8bbed7d_720w.webp (720×619) (zhimg.com)

此時我們發現,\(2\to3\)\(3\to2\) 我們都走了,可以認為是這條邊上的正向邊與反向邊互相抵消了,這樣我們實際得出的路徑就正好是 \(1\to2\to4,1\to3\to4\) 兩條。

因此反向邊的實質就是一種撤銷,由於反向邊時刻在加上正向邊丟失的容量,它就代表著可以撤回的容量。

這就是 FF 演算法的核心思想:殘量網路和反向邊。

接下來考慮如何實現這個演算法,顯然暴力 DFS 可行,但是時間複雜度過高,必須改進最佳化。

2.3 EK 演算法

即 Edmonds-Karp 演算法,利用 BFS 進行 FF 增廣。

EK 演算法的具體流程如下:

  • \(G_f\) 上如果可以從 \(s\) 出發走到 \(t\),代表我們找到了增廣路。
  • 在增廣路上,求出剩餘容量的最小值,給每條邊的流量加上它,同時給反向邊容量減去它。
  • 我們重複上述過程,直到沒有增廣路為止。

這就是 EK 演算法。單輪 BFS 增廣複雜度為 \(O(E)\),而增廣輪數上界為 \(O(VE)\),那麼 EK 演算法的總時間複雜度就是 \(O(VE^2)\)。當然這個複雜度是理論上界。

然而 FF 增廣和 EK 演算法都不是求最大流的主流演算法,真正最有用的是下面這個。

2.4 Dinic 演算法

Dinic 演算法是對於 FF/EK 演算法的最佳化,將兩者使用的 DFS 和 BFS 相結合。

我們在增廣前對 \(G_f\) 進行 BFS 分層,也就是根據節點 \(u\) 到源點 \(s\) 的距離 \(d(u)\) 將節點分為若干層。我們讓每個節點 \(u\) 都只向自己的下一層的節點 \(v\) 進行增廣,這樣我們每次增廣的都是原圖中邊數最少的路徑。

接下來我們進行 DFS,每次尋找到一條增廣路並更新到答案中,直到找不到增廣路後回溯。

有了這樣的指導,我們可以寫出一個最最樸素(可以說是錯誤)的 Dinic 出來:

#include <bits/stdc++.h>

using namespace std;

typedef long long LL;
const int Maxn = 2e5 + 5;
const int Inf = 2e9;

int n, m, s, t;

int head[Maxn], edgenum = 1;//和 tarjan 求橋很像,利用 edgenum=1 判斷雙向邊 
struct node {
	int nxt, to, w;
}edge[Maxn];

void add(int from, int to, int w) {//加邊要加雙向邊 
	edge[++edgenum] = {head[from], to, w};
	head[from] = edgenum;
	edge[++edgenum] = {head[to], from, 0};
	head[to] = edgenum;
}

int dis[Maxn];

bool bfs() {
	for(int i = 1; i <= n; i++) dis[i] = 0;
	queue <int> q;
	q.push(s);
	dis[s] = 1;
	while(!q.empty()) {
		int x = q.front();
		q.pop();
		for(int i = head[x]; i; i = edge[i].nxt) {
			int to = edge[i].to, w = edge[i].w;
			if(w > 0 && dis[to] == 0) {//找到還未遍歷且還有剩餘容量的 
				dis[to] = dis[x] + 1;//預處理距離 
				if(to == t) return 1;//找到一條 s->t 的路徑 
				q.push(to);
			}
		}
	}
	return 0;//不存在增廣路了 
}

int dfs(int x, int flow) {//上一層傳入的流量 
	if(x == t) return flow;//找到匯點就返回 
	int rest = flow;//殘量網路 
	for(int i = head[x]; i; i = edge[i].nxt) {
		int to = edge[i].to, w = edge[i].w;
		if(w > 0 && dis[to] == dis[x] + 1) {//還有剩餘容量且在下一層 
			int k = dfs(to, min(rest, w));//計算下面的層能經過的流量,也就是當前點能經過的流量
			rest -= k;//剩餘容量減少 
			edge[i].w -= k;
			edge[i ^ 1].w += k;//反向剩餘容量增加 
		}
	}
	return flow - rest;//返回當前節點能經過的流量 
}

int ans = 0;

void dinic() {
	while(bfs()) {//重複找有無增廣路,建立分層圖 
		ans += dfs(s, Inf);//從源點出發找 
	}
}

int main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin >> n >> m >> s >> t;
	for(int i = 1; i <= m; i++) {
		int u, v, w;
		cin >> u >> v >> w;
		add(u, v, w);
	}
	dinic();
	cout << ans << '\n';
	return 0;
}

我們發現,此時的 Dinic 使用的 DFS 本質上還是一個暴力,複雜度也並不優秀,因此我們在上面說這是一個錯誤的 Dinic。實際上,它並不完整,我們還需要一個東西:當前弧最佳化。

觀察程式碼,我們發現如果有一個節點 \(u\),入邊和出邊都很多,那麼每一次 \(u\) 接受來自入邊的流量都要遍歷出邊決定將流量傳遞給誰,這樣會使得時間複雜度驟增。我們考慮這樣一種現象:如果對於一條邊 \((u,v)\),它已經被增廣到了極限(邊 \((u,v)\) 沒有剩餘容量或 \(v\) 後側已經不可能再被增廣),此時再走 \((u,v)\) 就變得毫無意義。

放到 Dinic 中來看,我們對於一個節點下一層的節點,一定會把這個節點之後的邊的剩餘容量榨乾,此時我們就不需要再走這個節點了。因此對於每個節點 \(u\),維護在它的出邊中第一條還需要嘗試的邊,程式碼中體現為原本的 head[x] 換成這個指標 cur[x]

於是我們就可以保證 Dinic 的正確時間複雜度。對於單次 DFS,我們可以找到不超過 \(E\) 條的最短路徑,每一條最短路徑回溯不超過 \(V\) 次,同時當前弧最佳化保證不會經過相同節點。最佳化後每一次遍歷的最短路徑長度都會至少加一,因此至多尋找 \(V\) 次,所以複雜度為 \(O(V^2E)\)

但是仔細思考會發現,如果一個圖要滿足上面提到的所有 “不超過” 條件來卡滿複雜度是比較困難的。實際運用中,很少會有人專門卡 Dinic,因此這個複雜度僅僅是一個理論上限,大部分圖中,Dinic 的表現都十分優秀。

當然 Dinic 還有一些別的常數最佳化,如下:

  • 剩餘流量判斷:如果上一層節點傳遞的流量已經消耗完了,就不用再進行 DFS。
  • 多路增廣:如果我們找到了一條增廣路,那麼我們不用回到 \(s\) 重新遍歷,如果還剩下多餘的容量沒有用,我們繼續再該點嘗試找到更多增廣路。其實這一最佳化在 DFS 中是完全自然而看起來非常簡單的,但是它確實是一種常數最佳化。

所以最終版本的 Dinic 程式碼如下:

#include <bits/stdc++.h>
#define int long long

using namespace std;

typedef long long LL;
const int Maxn = 2e5 + 5;
const int Inf = 2e9;

int n, m, s, t;

int head[Maxn], edgenum = 1;//和 tarjan 求橋很像,利用 edgenum=1 判斷雙向邊 
struct node {
	int nxt, to, w;
}edge[Maxn];

int cur[Maxn];//當前弧 

void add(int from, int to, int w) {//加邊要加雙向邊 
	edge[++edgenum] = {head[from], to, w};
	head[from] = edgenum;
	edge[++edgenum] = {head[to], from, 0};
	head[to] = edgenum;
}

int dis[Maxn];

bool bfs() {
	for(int i = 1; i <= n; i++) {
		dis[i] = 0;
		cur[i] = head[i];//當前弧最佳化的初始化 
	}
	queue <int> q;
	q.push(s);
	dis[s] = 1;
	while(!q.empty()) {
		int x = q.front();
		q.pop();
		for(int i = head[x]; i; i = edge[i].nxt) {
			int to = edge[i].to, w = edge[i].w;
			if(w > 0 && dis[to] == 0) {//找到還未遍歷且還有剩餘容量的 
				dis[to] = dis[x] + 1;//預處理距離 
				if(to == t) return 1;//找到一條 s->t 的路徑 
				q.push(to);
			}
		}
	}
	return 0;//不存在增廣路了 
}

int dfs(int x, int flow) {//上一層傳入的流量 
	if(x == t) return flow;//找到匯點就返回 
	int rest = flow;//殘量網路 
	for(int i = cur[x]; i && rest/*剩餘容量判斷*/; i = edge[i].nxt) {
		cur[x] = i;//當前弧最佳化 
		int to = edge[i].to, w = edge[i].w;
		if(w > 0 && dis[to] == dis[x] + 1) {//還有剩餘容量且在下一層 
			int k = dfs(to, min(rest, w));//找到下面的層能經過的流量 
			rest -= k;//剩餘容量減少 
			edge[i].w -= k;
			edge[i ^ 1].w += k;//反向剩餘容量增加 
		}
	}
	return flow - rest;//返回當前節點能經過的流量 
}

int ans = 0;

void dinic() {
	while(bfs()) {//重複找有無增廣路,建立分層圖 
		ans += dfs(s, Inf);//從源點出發找 
	}
}

signed main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin >> n >> m >> s >> t;
	for(int i = 1; i <= m; i++) {
		int u, v, w;
		cin >> u >> v >> w;
		add(u, v, w);
	}
	dinic();
	cout << ans << '\n';
	return 0;
}

相關文章