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

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

目錄

1. 前言

本篇博文為 EK 演算法求解最大流。

在往下看之前,請先確保您已經瞭解網路流的一些基礎定義,包括但不限於網路,流量,源點,匯點,最大流定義。

如果您對上述定義有一部分不瞭解,可以前往這篇博文檢視:演算法學習筆記:網路流#1——有關內容+演算法導航

2. 例題

模板題:P3376 【模板】網路最大流

P.S.:洛谷還有一道加強版最大流,那道題只能使用 HLPP(最高標號預留推進)加上各種卡常優化來寫,但是那個不在討論範圍之內。

PP.S.:如果您想知道那道題到底卡常有多瘋狂的話,到

那道題 的題解區裡面看一下就知道了。

2.1 詳解

假設現在來了一張網路和一隻猴子。

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

現在這隻猴子想要求解這張圖的最大流。

於是這隻猴子選了下面這一條紅色路徑,匯點 \(t\) 增加 1 流量:

然後這隻猴子想要繼續增加匯點流量(下稱『推流』),選了下面這條路徑。結果它發現不能推流了,於是它得出答案為 1,然後去吃水果了。

但是聰明的您一定能夠發現,正確答案是 2,如下圖所示:

猴子吃完水果後發現您得到了正確的最大流,很生氣,於是它決定採用爆搜形式解決這道題。

解決完後它得意洋洋的走了,但是我們不滿足於爆搜啊,肯定要找到一種更加優秀的寫法。

先反觀一下上述解法,您會發現猴子錯誤的原因:做事太快了,都不帶思考。

沒辦法,程式不能思考,但是我們可以讓程式反悔!

什麼意思呢?

比如說還是這張圖,還是這條路徑,但是我們動一點手腳:

發現什麼了嗎?沒錯,這張圖上出現了反向邊。

反向邊有什麼用呢?

比如說還是這隻猴子,在獲得反向邊加持之後,還想走下面這條路徑。

於是它將 \(s\)\(2\) 運輸了 1 流量之後,發現正向邊沒得走了,但是存在一條反向邊 \((2,1)\)。於是它就走了,然後得到了正確結果 2。

您可以這樣理解反向邊的作用:

比如現在有 \(INF\) 個人要從 \(s\)\(t\),邊的流量表示這條路上當前還有幾張火車票。假設他們只能坐火車。

於是在 1 個人走了 \(s->1->2->t\) 之後,又有一個人想走 \(s->2->t\)

結果他發現走到 \(2\) 之後沒票了!這怎麼辦啊,不行不行。

於是他通過 \(2->1\) 的反向邊發現有一個人是從 \(1\) 來的,而且這個人可以走 \(1->t\) 來到達 \(t\)

於是他與之前的人協商了一下,那個人退了票,然後走了 \(1->t\)

於是就有兩個人可以到達 \(t\)

也就是說,反向邊可以提供反悔功能。

但是注意到上面那句加粗的話了嗎?

接下來給出 網路流 中『增廣路』的定義:

  • 增廣路:如果存在一條路使得 \(s\) 能夠到達 \(t\)\(t\) 的流量增加,那麼這條路就叫做增廣路。

於是上面的做法就是不斷尋找增廣路,直到找不到為止。

但是這為什麼是正確的?

2.2 正確性證明

本證明同時證明了最大流=最小割。

證明如下:

考慮割掉一些邊,使得源點與匯點不連通且點集個數為 2。

\(s\) 所在的點集為 \(S\)\(t\) 所在的點集為 \(T\)

顯然,進入 \(t\) 的總流量是由 \(T\) 內的點給的,而流出 \(s\) 的流量經過被割掉的邊進入了 \(T\)

於是我們有這樣一個式子:

  • 原先 \(S\)\(T\) 的流量 = 網路總流量

顯然,無論怎樣構造割,都有這個結果。

那麼假設我們知道最大流為 \(Flow\)

  • 在任意構造的割 \(S\)\(T\) 中,必有:原先 \(S\)\(T\) 的流量 \(\leq Flow\)

那麼現在,在經過上述過程之後,網路沒有增廣路了。

令當前的網路叫做『殘量網路』,那麼在殘量網路中,令 \(S\) 表示 \(s\) 能夠到達的所有點,\(T\)\(S\)\(V\) 中的補集,\(V\) 為點集。

那麼,在殘量網路中 \(S\)\(T\) 流動的流量 = 原先這些被割斷的邊的容量。

根據上面所證明的,有:網路總流量 = 原先這些被割斷的邊的流量。

考慮到所有割流量 \(\leq Flow\),此時達到的不等式的交界處。

因此,此時得到的網路總流量必為最大流,同時證明了 最小割 = 最大流,證畢。

2.3 做法與程式碼

實現就很關鍵了。

在尋找增廣路的時候,有兩種演算法:DFS 與 BFS。

選擇哪種演算法,將會直接導致程式是 TLE 還是 AC。

為什麼這麼說呢?

先看看 DFS。

如果採用 DFS 來實現上述不斷尋找增廣路的過程,叫做 FF 演算法。

大體思路就是不斷增廣,尋找增廣路,沒有了就結束演算法。

很遺憾的是,FF 演算法被卡掉了,幾乎跟暴力沒啥兩樣。

為什麼?究其原因,就是因為 DFS 的效率太低了!

首先考慮每一次我們只能找出一條增廣路,DFS 的複雜度至少 \(O(n)\),如果繞的路足夠長或者有環,FF 就會徹底 TLE,其實就相當於是跟猴子一樣不斷暴力,

那麼 BFS 呢?

在學搜尋的時候各位應該都聽過這麼一句話:同等狀態下,BFS 比 DFS 要優。

如果採用 BFS 寫法,大體思路仍然是不斷尋找增廣路,但是因為 BFS 不會陷入環的陷阱,效率就高了很多。

而這個演算法,就叫做 EK 演算法。

如果思路理解了,程式碼應該也是好理解的吧qwq

關於存邊與建反向邊:

需要注意的是,邊的編號要從 2 開始。

為什麼呢?這樣可以巧妙利用異或的性質:偶數異或 1 就是加一,奇數異或 1 就是減 1。

採取從 2 開始,那麼第一組邊的編號是 2,3,第二組邊是 4,5。

於是在取反向邊和改變邊權的時候就很方便了。

還有,強烈推薦使用鏈式前向星,因為 vector 存圖在網路流裡面簡直是太!難!寫!了!

程式碼:

/*
========= Plozia =========
	Author:Plozia
	Problem:P3376 【模板】網路最大流——EK 寫法
	Date:2021/3/18
========= Plozia =========
*/

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

typedef long long LL;
const int MAXN = 200 + 10, MAXM = 5000 + 10;
LL INF = 0x7f7f7f7f7f7f7f7f;
int n, m, s, t, cnt_Edge = 1, Head[MAXN], pre[MAXN], vis[MAXN];
//pre:走過來的邊的編號 vis:有沒有訪問過,防止重複走增廣路
LL dis[MAXN], ans;//dis:當前點的流量
struct node
{
	int to; LL val; int Next;
}Edge[MAXM << 1];//注意 * 2

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;
}
void add_Edge(int x, int y, int z) {Edge[++cnt_Edge] = (node){y, (LL)z, Head[x]}; Head[x] = cnt_Edge;}
LL Min(LL fir, LL sec) {return (fir < sec) ? fir : sec;}

bool bfs()
{
	queue <int> q; q.push(s);
	memset(vis, 0, sizeof(vis));//清空
	vis[s] = 1; dis[s] = INF;//初始點流量為無窮大
	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) continue;//只考慮大於 0 的邊
			if (vis[u] == 1) continue;//不重複走增廣路
			pre[u] = i; vis[u] = 1;//標記
			dis[u] = Min(dis[x], Edge[i].val);//推流
			q.push(u);
			if (u == t) return 1;//找到增廣路
		}
	}
	return 0;//沒有增廣路了
}

void update()
{
	for (int i = t; i != s;)
	{
		int v = pre[i];
		Edge[v].val -= dis[t]; Edge[v ^ 1].val += dis[t];
		i = Edge[v ^ 1].to;
	}
	ans += dis[t];//更新增廣路上的邊權
}

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()) {update();}//不斷找增廣路
	printf("%lld\n", ans);
	return 0;
}

3. 總結

EK 求解最大流的核心思路就是利用 BFS 不斷尋找增廣路,來更新一路上的邊權。

當然,EK 比較容易被卡,所以接下來將會介紹一種基於 FF 思路的更強的演算法:dinic 演算法。

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