1. 程式人生 > 其它 >圖論專題-網路流-學習筆記:dinic 求解最大流

圖論專題-網路流-學習筆記:dinic 求解最大流

目錄

1. 前言

本篇博文講解求解最大流的 dinic 演算法。

在學這篇博文之前,請先確保掌握以下知識:

  1. 網路流的一些基礎定義,參見:演算法學習筆記:網路流#1——有關內容+演算法導航
  2. FF 與 EK 求解最大流的思路,參見:演算法學習筆記:網路流#2——EK 求解最大流

下面假設讀者已經掌握上述內容。

先來回顧 EK 求解最大流的思路:利用 BFS 不斷尋找增廣路,不斷推流,直到找不到增廣路為止。

而 FF 求解最大流的思路:利用 DFS 不斷尋找增廣路,不斷推流,直到找不到增廣路為止。

上文中作者提到過,FF 求解最短路效率低下的問題就是因為 DFS 複雜度太高了。

但是:

Dfs 的實現方式雖然暫時無法取得好的效果,但我們並不應該就此放棄,Dfs 相對 Bfs 靈活的架構必能給予我們廣闊的優化空間。——《資訊學奧賽一本通——提高篇》

所以我們要想想如何優化 DFS。

發明 EK 演算法的科學家 Dinic 也意識到了這一點,於是他就在 FF 的基礎上加以改進,提出了大名鼎鼎的 dinic 演算法,也是目前網路流的主流演算法之一,甚至有的人這麼說:99% 的網路流問題,都可以用 dinic 通過。

沒錯,FF 是 Dinic 發明的

接下來就開始講解吧!

2. 模板

模板題:

P3376 【模板】網路最大流

2.1 詳解

首先我們需要知道 DFS 複雜度為什麼高。

假設當前網路構成了一個環,而且邊的流量都是 \(INF\),於是不管這個環裡面流進了多少流量,終將無休止的迴圈。

那麼怎樣解決這個問題呢?最顯然的一個思路就是控制一下迴圈層數。

當然這樣程式碼太煩了,於是就有另外一個思路:能不能控制一下點的去向呢?

換句話說,能不能控制一下大體流量的走向呢?

這就是 dinic 演算法的主要思路。

dinic 演算法的第一步,便是對網路分層。

換句話說,從源點開始,標記源點深度為 1,其能到達的點深度為 2,以此類推。

然後,再開始 DFS,規定只能從層數小的流向層數大的。

但是這樣仍然有一個問題,看圖:

(繪圖工具:Windows 10 的 Microsoft Whiteboard)

在上圖中,您會發現:如果 1 號點不幸的選擇了 2 號點,那麼就要繞一大圈才能到 \(t\),但是實際上只需要經過 150 號點就可以了呀!

因此為了防止這種情況,規定:流量只能在相鄰兩層之間流動。

這樣就可以完美避免了這個問題,但是正確性呢?

仍然正確!見後面的正確性證明。

現在先回到這個問題,假設有一個點推流推不出去了,這說明什麼?

這說明這個點已經與匯點不在連通了!這個時候,需要將這個點的層數改為 0,節省時間。

2.2 正確性證明

證明如下:

考慮 FF/EK 演算法的終止條件是沒有增廣路,那麼 dinic 也是沒有增廣路。

在這樣分層之後,如果圖中還有增廣路,那麼在 BFS 分層的時候一定會擴充套件到這條增廣路,而最後的終止條件就是沒有增廣路。

證畢。

2.3 程式碼

程式碼如下:

/*
========= Plozia =========
	Author:Plozia
	Problem:P3376 【模板】網路最大流——dinic 求解
	Date:2021/3/18
========= Plozia =========
*/

#include <bits/stdc++.h>
using std::queue;

typedef long long LL;
const int MAXN = 200 + 10, MAXM = 5000 + 10;
const LL INF = 0x7f7f7f7f7f7f7f7f;
int n, m, s, t, Head[MAXN], cnt_Edge = 1, dep[MAXN];
struct node {int to; LL val; int Next;} Edge[MAXM << 1];
bool vis[MAXN];
LL ans;
LL Min(LL fir, LL sec) {return (fir < sec) ? fir : sec;}
void add_Edge(int x, int y, int z) {Edge[++cnt_Edge] = (node){y, (LL)z, Head[x]}; Head[x] = cnt_Edge;}

int read()
{
	int sum = 0, fh = 1; char ch = getchar();
	for (; ch < '0' || ch > '9'; ch = getchar()) fh -= (ch == '-') << 1;
	for (; ch >= '0' && ch <= '9'; ch = getchar()) sum = (sum << 3) + (sum << 1) + (ch ^ 48);
	return (fh == 1) ? sum : -sum;
}

bool bfs()
{
	queue <int> q; q.push(s);
	memset(vis, 0, sizeof(vis));
	memset(dep, 0, sizeof(dep));
	dep[s] = 1; vis[s] = 1;
	while (!q.empty())
	{
		int x = q.front(); q.pop();
		for (int i = Head[x]; i; i = Edge[i].Next)
		{
			int u = Edge[i].to;
			if (Edge[i].val == 0 || vis[u]) continue;//只考慮有流量的點
			vis[u] = 1; dep[u] = dep[x] + 1;
			q.push(u);
		}
	}
	return dep[t];
}

LL dfs(int now, LL Flow)
{
	if (now == t) return Flow;
	LL used = 0;
	for (int i = Head[now]; i; i = Edge[i].Next)//能推流就推流
	{
		int u = Edge[i].to;
		if (Edge[i].val && dep[u] == dep[now] + 1)
		{
			LL Minn = dfs(u, Min(Flow - used, Edge[i].val));
			Edge[i].val -= Minn; Edge[i ^ 1].val += Minn; used += Minn;
			if (used == Flow) return used;
		}
	}
	if (used == 0) dep[now] = 0;//修改層數
	return used;
}

int main()
{
	n = read(), m = read(), s = read(), t = read();
	for (int i = 1; i <= m; ++i)
	{
		int x = read(), y = read(), z = read();
		add_Edge(x, y, z); add_Edge(y, x, 0);
	}
	while (bfs()) ans += dfs(s, INF);//不斷找增廣路
	printf("%lld\n", ans);
	return 0;
}

2.4 優化

dinic 演算法的很重要的一個優化就是當前弧優化。

當前弧優化的意思是這樣的:

每一次增廣路的時候記錄一下當前走到哪一條邊(弧)了,下一次到這個點推流的時候直接從這條邊開始遍歷。

為什麼是正確的呢?

顯然,已經遍歷過的邊肯定已經推完了所有流量,也就是再推流也沒有用,只有當前這條邊可能可以再推流,那麼從這條邊開始遍歷即可。

當前弧優化的程式碼如下:

/*
========= Plozia =========
	Author:Plozia
	Problem:P3376 【模板】網路最大流——dinic 求解
	Date:2021/3/18
========= Plozia =========
*/

#include <bits/stdc++.h>
using std::queue;

typedef long long LL;
const int MAXN = 200 + 10, MAXM = 5000 + 10;
const LL INF = 0x7f7f7f7f7f7f7f7f;
int n, m, s, t, Head[MAXN], cnt_Edge = 1, dep[MAXN], cur[MAXN];
struct node {int to; LL val; int Next;} Edge[MAXM << 1];
bool vis[MAXN];
LL ans;
LL Min(LL fir, LL sec) {return (fir < sec) ? fir : sec;}
void add_Edge(int x, int y, int z) {Edge[++cnt_Edge] = (node){y, (LL)z, Head[x]}; Head[x] = cnt_Edge;}

int read()
{
	int sum = 0, fh = 1; char ch = getchar();
	for (; ch < '0' || ch > '9'; ch = getchar()) fh -= (ch == '-') << 1;
	for (; ch >= '0' && ch <= '9'; ch = getchar()) sum = (sum << 3) + (sum << 1) + (ch ^ 48);
	return (fh == 1) ? sum : -sum;
}

bool bfs()
{
	queue <int> q; q.push(s);
	memset(vis, 0, sizeof(vis));
	memset(dep, 0, sizeof(dep));
	dep[s] = 1; vis[s] = 1;
	while (!q.empty())
	{
		int x = q.front(); q.pop();
		for (int i = Head[x]; i; i = Edge[i].Next)
		{
			int u = Edge[i].to;
			if (Edge[i].val == 0 || vis[u]) continue;//只考慮有流量的點
			vis[u] = 1; dep[u] = dep[x] + 1;
			q.push(u);
		}
	}
	return dep[t];
}

LL dfs(int now, LL Flow)
{
	if (now == t) return Flow;
	LL used = 0;
	for (int i = cur[now]; i; i = Edge[i].Next)//能推流就推流
	{
		cur[now] = i;//當前弧優化
		int u = Edge[i].to;
		if (Edge[i].val && dep[u] == dep[now] + 1)
		{
			LL Minn = dfs(u, Min(Flow - used, Edge[i].val));
			Edge[i].val -= Minn; Edge[i ^ 1].val += Minn; used += Minn;
			if (used == Flow) return used;
		}
	}
	if (used == 0) dep[now] = 0;//修改層數
	return used;
}

int main()
{
	n = read(), m = read(), s = read(), t = read();
	for (int i = 1; i <= m; ++i)
	{
		int x = read(), y = read(), z = read();
		add_Edge(x, y, z); add_Edge(y, x, 0);
	}
	while (bfs()) {for (int i = 1; i <= n; ++i) cur[i] = Head[i]; ans += dfs(s, INF);}//不斷找增廣路
	printf("%lld\n", ans);
	return 0;
}

3. 總結

dinic 演算法的基本步驟:利用 BFS 分層,然後利用 DFS 不斷尋找增廣路,能推流就推流。

實際上,dinic 演算法已經足夠高效了,但是很遺憾的是 dinic 在特殊構造的資料下仍然會被卡掉,這個時候就需要一種更高效的演算法:ISAP 出場了!

傳送門:演算法學習筆記:網路流#4——ISAP 求解最大流