一.網絡流:流&網絡&割
1.網絡流問題(NetWork Flow Problem):
給定指定的一個有向圖,其中有兩個特殊的點源S(Sources)和匯T(Sinks),每條邊有指定的容量(Capacity),求滿足條件的從S到T的最大流(MaxFlow).
下面給出一個通俗點的解釋
(下文基本避開形式化的證明 基本都用此類描述敘述)
好比你家是匯 自來水廠(有需要的同學可以把自來水廠當成銀行之類 以下類似)是源
然後自來水廠和你家之間修了很多條水管子接在一起 水管子規格不一 有的容量大 有的容量小
然後問自來水廠開閘放水 你家收到水的最大流量是多少
如果自來水廠停水了 你家那的流量就是0 當然不是最大的流量
但是你給自來水廠交了100w美金 自來水廠拼命水管裏通水 但是你家的流量也就那麼多不變了 這時就達到了最大流
-------------------------------------------------------------------------------------------------------------
2.三個基本的性質:
如果 C代表每條邊的容量 F代表每條邊的流量一個顯然的實事是F小於等於C 不然水管子就爆了,這就是網絡流的第一條性質 容量限制(Capacity Constraints):F<x,y> ≤ C<x,y>
再考慮節點任意一個節點 流入量總是等於流出的量 否則就會蓄水(爆炸危險...)或者平白無故多出水(有地下水涌出?)
這是第二條性質 流量守恆(Flow Conservation):Σ F<v,x> = Σ F<x,u>
當然源和匯不用滿足流量守恆 我們不用去關心自來水廠的水是河裏的 還是江裏的
最後一個不是很顯然的性質 是斜對稱性(Skew Symmetry): F<x,y> = - F<y,x>
這其實是完善的網絡流理論不可缺少的 就好比中學物理裏用正負數來定義一維的位移一樣
百米起點到百米終點的位移是100m的話 那麼終點到起點的位移就是-100m
同樣的 x向y流了F的流 y就向x流了-F的流
對於任意一個時刻,設f(u,v)實際流量,則整個圖G的流網絡滿足3個性質:
1. 容量限制:對任意u,v∈V,f(u,v)≤c(u,v)。
2. 反對稱性:對任意u,v∈V,f(u,v) = -f(v,u)。從u到v的流量一定是從v到u的流量的相反值。
3. 流守恆性:對任意u,若u不爲S或T,一定有∑f(u,v)=0,(u,v)∈E。即u到相鄰節點的流量之和爲0,因爲流入u的流量和u點流出的流量相等,u點本身不會"製造"和"消耗"流量。
-------------------------------------------------------------------------------------------------------------3.容量網絡&流量網絡&殘留網絡:
網絡就是有源匯的有向圖 關於什麼就是指邊權的含義是什麼容量網絡就是關於容量的網絡 基本是不改變的(極少數問題需要變動)
流量網絡就是關於流量的網絡 在求解問題的過程中
通常在不斷的改變 但是總是滿足上述三個性質
調整到最後就是最大流網絡 同時也可以得到最大流值
殘留網絡往往概括了容量網絡和流量網絡 是最爲常用的
殘留網絡=容量網絡-流量網絡
這個等式是始終成立的 殘留值當流量值爲負時甚至會大於容量值
流量值爲什麼會爲負?有正必有負,記住斜對稱性!
4.割&割集:
無向圖的割集(Cut Set):C[A,B]是將圖G分爲A和B兩個點集 A和B之間的邊的全集網絡的割集:C[S,T]是將網絡G分爲s和t兩部分點集 S屬於s且T屬於t 從S到T的邊的全集
帶權圖的割(Cut)就是割集中邊或者有向邊的權和
通俗的理解一下:
割集好比是一個恐怖分子 把你家和自來水廠之間的水管網絡砍斷了一些
然後自來水廠無論怎麼放水 水都只能從水管斷口嘩嘩流走了 你家就停水了
割的大小應該是恐怖分子應該關心的事 畢竟細管子好割一些
而最小割花的力氣最小
==================================================================================
二.計算最大流的基本算法
那麼怎麼求出一個網絡的最大流呢?這裏介紹一個最簡單的算法:Edmonds-Karp算法 即最短路徑增廣算法 簡稱EK算法
EK算法基於一個基本的方法:Ford-Fulkerson方法 即增廣路方法 簡稱FF方法
增廣路方法是很多網絡流算法的基礎 一般都在殘留網絡中實現
其思路是每次找出一條從源到匯的能夠增加流的路徑 調整流值和殘留網絡 不斷調整直到沒有增廣路爲止
FF方法的基礎是增廣路定理(Augmenting Path Theorem):網絡達到最大流當且僅當殘留網絡中沒有增廣路
證明略 這個定理應該能夠接受的吧
EK算法就是不斷的找最短路 找的方法就是每次找一條邊數最少的增廣 也就是最短路徑增廣
這樣就產生了三個問題:
-------------------------------------------------------------------------------------------------------------
1.最多要增廣多少次?
可以證明 最多O(VE)次增廣 可以達到最大流 證明略2.如何找到一條增廣路?
先明確什麼是增廣路 增廣路是這樣一條從s到t的路徑 路徑上每條邊殘留容量都爲正把殘留容量爲正的邊設爲可行的邊 那麼我們就可以用簡單的BFS得到邊數最少的增廣路
3.如何增廣?
BFS得到增廣路之後 這條增廣路能夠增廣的流值 是路徑上最小殘留容量邊決定的把這個最小殘留容量MinCap值加到最大流值Flow上 同時路徑上每條邊的殘留容量值減去MinCap
最後 路徑上每條邊的反向邊殘留容量值要加上MinCap 爲什麼? 下面會具體解釋
-------------------------------------------------------------------------------------------------------------
這樣每次增廣的複雜度爲O(E) EK算法的總複雜度就是O(VE^2),事實上 大多數網絡的增廣次數很少 EK算法能處理絕大多數問題,平均意義下增廣路算法都是很快的
增廣路算法好比是自來水公司不斷的往水管網裏一條一條的通水
上面還遺留了一個反向邊的問題: 爲什麼增廣路徑上每條邊的反向邊殘留容量值要加上MinCap?
*********************************************************************************************
因爲斜對稱性! 由於殘留網絡=容量網絡-流量網絡
容量網絡不改變的情況下
由於增廣好比給增廣路上通了一條流 路徑說所有邊流量加MinCap
流量網絡中路徑上邊的流量加MinCap 反向邊流量減去MinCap
相對應的殘留網絡就發生相反的改變
*********************************************************************************************
這樣我們就完成了EK算法 具體實現可以用鄰接表存圖 也可以用鄰接矩陣存圖
鄰接表存圖 由於流量同時存在於邊與反向邊 爲了方便求取反向邊 建圖把一對互爲反向邊的邊建在一起
代碼很簡單 最好自己實現一下
看一個具體的增廣路算法的例子吧
=====================================================================
三.最大流最小割定理
下面介紹網絡流理論中一個最爲重要的定理最大流最小割定理(Maximum Flow, Minimum Cut Theorem):網絡的最大流等於最小割
具體的證明分三部分
1.任意一個流都小於等於任意一個割
這個很好理解 自來水公司隨便給你家通點水 構成一個流恐怖分子隨便砍幾刀 砍出一個割
由於容量限制 每一根的被砍的水管子流出的水流量都小於管子的容量
每一根被砍的水管的水本來都要到你家的 現在流到外面 加起來得到的流量還是等於原來的流
管子的容量加起來就是割 所以流小於等於割
由於上面的流和割都是任意構造的 所以任意一個流小於任意一個割
2.構造出一個流等於一個割
當達到最大流時 根據增廣路定理殘留網絡中s到t已經沒有通路了 否則還能繼續增廣
我們把s能到的的點集設爲S 不能到的點集爲T
構造出一個割集C[S,T] S到T的邊必然滿流 否則就能繼續增廣
這些滿流邊的流量和就是當前的流即最大流
把這些滿流邊作爲割 就構造出了一個和最大流相等的割
3.最大流等於最小割
設相等的流和割分別爲Fm和Cm則因爲任意一個流小於等於任意一個割
任意F≤Fm=Cm≤任意C
定理說明完成,證明如下:
對於一個網絡流圖G=(V,E),其中有源點s和匯點t,那麼下面三個條件是等價的:
1. 流f是圖G的最大流
2. 殘留網絡Gf不存在增廣路
3. 對於G的某一個割(S,T),此時f = C(S,T)
首先證明1 => 2:
我們利用反證法,假設流f是圖G的最大流,但是殘留網絡中還存在有增廣路p,其流量爲fp。則我們有流f'=f+fp>f。這與f是最大流產生矛盾。
接着證明2 => 3:
假設殘留網絡Gf不存在增廣路,所以在殘留網絡Gf中不存在路徑從s到達t。我們定義S集合爲:當前殘留網絡中s能夠到達的點。同時定義T=V-S。
此時(S,T)構成一個割(S,T)。且對於任意的u∈S,v∈T,有f(u,v)=c(u,v)。若f(u,v)<c(u,v),則有Gf(u,v)>0,s可以到達v,與v屬於T矛盾。
因此有f(S,T)=Σf(u,v)=Σc(u,v)=C(S,T)。
最後證明3 => 1:
由於f的上界爲最小割,當f到達割的容量時,顯然就已經到達最大值,因此f爲最大流。
這樣就說明了爲什麼找不到增廣路時,所求得的一定是最大流。
=====================================================================網絡流入門
基本概念(從書上摘抄,可以直接跳過不看)
容量網絡和網絡最大流
容量網絡: 設 G(V, E)
是一個有向網絡, 在 V 中指定了一個頂點, 稱爲源點(記爲 Vs ), 以及另一個頂點, 稱爲匯點(記爲
Vt); 對於每一條弧 <u, v>∈E
, 對應有一個權值 c(u, v)>0, 稱爲弧的容量
,
通常把這樣的有向網絡 G 稱爲容量網絡。
也就是指: 一個擁有源點、匯點並且可以容納流量的圖.
弧的流量: 通過容量網絡 G 中每條弧 <u, v>
上的實際流量(簡稱流量), 記爲 f(u,
v)
。
網絡流: 所有弧上流量的集合 f = { f(u, v) }
,稱爲該容量網絡 G 的一個網絡流。
可行流: 在容量網絡 G(V, E)
中, 滿足以下條件的網絡流 f, 稱爲可行流:
- 弧流量限制條件:
0≤f(u,v)≤c(u,v)
- 平衡條件: 除了 Vs, Vt 外, 其餘的點流入的流量總和等於流出的流量總和, 其中
Vs 流出的流量總和 - 流出的流量總和 = f
,Vt 流入的流量總和 - 流出的流量總和 = f
, 並且稱f
爲可性流的流量.
也就是指: 在圖中有一條從 Vs 到 Vt 的路徑, 這條路徑上起點
fo−fi=f , 終點fi−fo=f , 其他的點fi==fo , 並且所有的邊的當前流量小於等於最大流量.(其中fi 代表流入流量,fo 代表流出流量)
僞流: 如果一個網絡流只滿足弧流量限制條件, 不滿足平衡條件, 則這種網絡流稱爲僞流, 或稱爲容量可行流。
最大流: 在容量網絡 G(V, E)
中, 滿足弧流量限制條件和平衡條件、且具有最大流量的可行流, 稱爲網絡最大流, 簡稱最大流。
鏈與增廣路
在容量網絡 G(V, E)
中, 設有一可行流 f
= { f(u, v) }
, 根據每條弧上流量的多少、以及流量和容量的關係,可將弧分四種類型:
- 飽和弧, 即
f(u,v)=c(u,v) ; - 非飽和弧,即
f(u,v)<c(u,v) ; - 零流弧, 即
f(u,v)=0 ; - 非零流弧, 即
f(u,v)>0 。
鏈: 在容量網絡中,稱頂點序列<u, u1>
或 <u1,
u>
爲容量網絡中一條弧。沿着 Vs 到 Vt 的一條鏈, 各弧可分爲兩類:
- 前向弧: 方向與鏈的正方向一致的弧, 其集合記爲
P+
; - 後向弧: 方向與鏈的正方向相反的弧, 其集合記爲
P-
;
增廣路: 設 f 是一個容量網絡 G 中的一個可行流, P 是從 Vs 到 Vt 的一條鏈, 若 P 滿足下列條件:
- 在 P 的所有前向弧
<u, v>
上,0≤f(u,v)<c(u,v) , 即P+
中每一條弧都是非飽和弧; - 在 P 的所有後向弧
<u, v>
上,0<f(u,v)≤c(u,v) , 即P–
中每一條弧是非零流弧。
則稱 P 爲關於可行流 f 的一條增廣路, 簡稱爲 增廣路(或稱爲增廣鏈、可改進路)
。沿着增廣路改進可行流的操作稱爲增廣
殘留容量與殘留網絡
殘留容量: 給定容量網絡 G(V, E)
及可行流 f, 弧 <u,
v>
上的殘留容量記爲 <u, v>
上還有一個反方向的殘留容量
一個容量網絡中還可以壓入的流量稱爲殘留容量
殘留網絡: 設有容量網絡 G(V,
E)
及其上的網絡流 f,G 關於 f 的殘留網絡(簡稱殘留網絡)記爲 G'(V',
E')
, 其中 G’的頂點集 V’和 G 的頂點集 V 相同,即 V’=V, 對於 G 中的任何一條弧 <u,
v>
, 如果 <u, v>∈E'
, 其容量爲 <v, u>∈E'
, 其容量爲 剩餘網絡
.
由殘留的容量以及源點匯點構成的網絡。
割與最小割
割: 在容量網絡 G(V, E)
中, 設 E'⊆E
,
如果在 G 的基圖中刪去 E’ 後不再連通, 則稱 E’ 是 G 的割。割將 G 的頂點集 V 劃分成兩個子集 S 和 T = V - S。將割記爲(S, T)。
s-t 割: 更進一步, 如果割所劃分的兩個頂點子集滿足源點 Vs ∈ S
,匯點 Vt
∈ T
, 則稱該割爲 s-t 割
。 s-t 割(S, T)中的弧 <u,
v>(u∈S, v∈T)
稱爲割的前向弧, 弧 <u, v>( u∈T, v∈S)
稱爲割的反向弧。
割的容量: 設 (S, T)
爲容量網絡 G(V,
E)
的一個割, 其容量定義爲所有前向弧的容量總和, 用 c(S, T)
表示。
最小割: 容量網絡 G(V, E)
的最小割是指容量最小的割。
相關定理
殘留網絡與原網絡的關係
設 f 是容量網絡 G(V, E) 的可行流, f’ 是殘留網絡 G’ 的可行流, 則 f + f’ 仍是容量網絡 G 的一個可行流。(f + f’ 表示對應弧上的流量相加)
網絡流流量與割的淨流量之間的關係
在一個容量網絡 G(V, E) 中, 設其任意一個流爲 f, 關於 f 的任意一個割爲(S, T), 則有
網絡流流量與割的容量之間的關係
在一個容量網絡 G(V, E) 中, 設其任意一個流爲 f, 任意一個割爲(S, T), 則必有
最大流最小割定理
對容量網絡 G(V, E), 其最大流的流量等於最小割的容量。
增廣路定理
設容量網絡 G(V, E) 的一個可行流爲 f, f 爲最大流的充要條件是在容量網絡中不存在增廣路。
幾個等價命題
設容量網絡 G(V, E)的一個可行流爲 f 則:
- 1) f 是容量網絡 G 的最大流;
- 2) | f |等於容量網絡最小割的容量;
- 3) 容量網絡中不存在增廣路;
- 4) 殘留網絡 G’中不存在從源點到匯點的路徑。
最大流
最大流相關算法有兩種解決思想, 一種是增廣路算法
思想, 另一種是預流推進
算法思想。
下面將分別介紹這兩種算法思想。
增廣路算法(Ford-Fulkerson)
基本思想
根據增廣路定理, 爲了得到最大流, 可以從任何一個可行流開始, 沿着增廣路對網絡流進行增廣, 直到網絡中不存在增廣路爲止,這樣的算法稱爲增廣路算法。問題的關鍵在於如何有效地找到增廣路, 並保證算法在有限次增廣後一定終止。
增廣路算法的基本流程是 :
- (1) 取一個可行流 f 作爲初始流(如果沒有給定初始流,則取零流 f= { 0 }作爲初始流);
- (2) 尋找關於 f 的增廣路 P,如果找到,則沿着這條增廣路 P 將 f 改進成一個更大的流, 並建立相應的反向弧;
- (3) 重複第(2)步直到 f 不存在增廣路爲止。
增廣路算法的關鍵是 尋找增廣路
和 改進網絡流
。
問題: 爲什麼要創建反向弧呢?
原因: 爲程序提供一次反悔的機會 什麼意思, 如下圖所示:
在圖中如果程序找到了一條增廣路 1 -> 2 -> 4 -> 6, 此時得到一個流量爲 2 的流並且無法繼續進行增廣,
但是如果在更新可行流的同時建立反向弧的話, 就可以找到 1 -> 3 -> 4 -> 2 -> 5 -> 6 的可行流, 流量爲1, 這樣就可以得到最大流爲 3.
一般增廣路算法(EdmondsKarp)
算法流程
在一般的增廣路算法中, 程序的實現過程與增廣路求最大流的過程基本一致. 即每一次更新都進行一次找增廣路然後更新路徑上的流量的過程。但是我們可以從上圖中發現一個問題, 就是每次找到的增廣路曲曲折折非常長, 此時我們往往走了冤枉路(即:明明我們可以從源點離匯點越走越進的,可是中間的幾條邊卻向離匯點遠的方向走了), 此時更新增廣路的複雜度就會增加。EK 算法爲了規避這個問題使用了 bfs 來尋找增廣路, 然後在尋找增廣路的時候總是向離匯點越來越近的方向去尋找下一個結點。
算法實現
鄰接矩陣
#include <queue>
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
const int MAXN = 300;
const int MAX_INT = ((1 << 31) - 1);
int n; // 圖中點的數目
int pre[MAXN]; // 從 s - t 中的一個可行流中, 節點 i 的前序節點爲 Pre[i];
bool vis[MAXN]; // 標記一個點是否被訪問過
int mp[MAXN][MAXN]; // 記錄圖信息
bool bfs(int s, int t){
queue <int> que;
memset(vis, 0, sizeof(vis));
memset(pre, -1, sizeof(pre));
pre[s] = s;
vis[s] = true;
que.push(s);
while(!que.empty()){
int u = que.front();
que.pop();
for(int i = 1; i <= n; i++){
if(mp[u][i] && !vis[i]){
pre[i] = u;
vis[i] = true;
if(i == t) return true;
que.push(i);
}
}
}
return false;
}
int EK(int s, int t){
int ans = 0;
while(bfs(s, t)){
int mi = MAX_INT;
for(int i = t; i != s; i = pre[i]){
mi = min(mi, mp[pre[i]][i]);
}
for(int i = t; i != s; i = pre[i]){
mp[pre[i]][i] -= mi;
mp[i][pre[i]] += mi;
}
ans += mi;
}
return ans;
}
鄰接表const int MAXN = 430;
const int MAX_INT = (1 << 30);
struct Edge{
int v, nxt, w;
};
struct Node{
int v, id;
};
int n, m, ecnt;
bool vis[MAXN];
int head[MAXN];
Node pre[MAXN];
Edge edge[MAXN];
void init(){
ecnt = 0;
memset(edge, 0, sizeof(edge));
memset(head, -1, sizeof(head));
}
void addEdge(int u, int v, int w){
edge[ecnt].v = v;
edge[ecnt].w = w;
edge[ecnt].nxt = head[u];
head[u] = ecnt++;
}
bool bfs(int s, int t){
queue <int> que;
memset(vis, 0, sizeof(vis));
memset(pre, -1, sizeof(pre));
pre[s].v = s;
vis[s] = true;
que.push(s);
while(!que.empty()){
int u = que.front();
que.pop();
for(int i = head[u]; i + 1; i = edge[i].nxt){
int v = edge[i].v;
if(!vis[v] && edge[i].w){
pre[v].v = u;
pre[v].id = i;
vis[v] = true;
if(v == t) return true;
que.push(v);
}
}
}
return false;
}
int EK(int s, int t){
int ans = 0;
while(bfs(s, t)){
int mi = MAX_INT;
for(int i = t; i != s; i = pre[i].v){
mi = min(mi, edge[pre[i].id].w);
}
for(int i = t; i != s; i = pre[i].v){
edge[pre[i].id].w -= mi;
edge[pre[i].id ^ 1].w += mi;
}
ans += mi;
}
return ans;
}
// 加邊
addEdge(u, v, w);
addEdge(v, u, 0);
// 調用
int ans = EK(s, t);
算法複雜度
每進行一次增廣需要的時間複雜度爲 bfs 的複雜度 + 更新殘餘網絡的複雜度, 大約爲 O(m)(m爲圖中的邊的數目), 需要進行多少次增廣呢, 假設每次增廣只增加1, 則需要增廣 nW 次(n爲圖中頂點的數目, W爲圖中邊上的最大容量), .
Dinic 算法
算法思想
DINIC 在找增廣路的時候也是找的最短增廣路, 與 EK 算法不同的是 DINIC 算法並不是每次 bfs 只找一個增廣路, 他會首先通過一次 bfs 爲所有點添加一個標號, 構成一個層次圖, 然後在層次圖中尋找增廣路進行更新。
算法流程
- 利用 BFS 對原來的圖進行分層,即對每個結點進行標號, 這個標號的含義是當前結點距離源點的最短距離(假設每條邊的距離都爲1),注意:構建層次圖的時候所走的邊的殘餘流量必須大於0
- 用 DFS 尋找一條從源點到匯點的增廣路, 注意: 此處尋找增廣路的時候要按照層次圖的順序, 即如果將邊(u, v)納入這條增廣路的話必須滿足
dis[u]=dis[v]−1 , 其中dis[i] 爲結點i 的編號。找到一條路後要根據這條增廣路徑上的所有邊的殘餘流量的最小值l 更新所有邊的殘餘流量(即正向弧 - l, 反向弧 + l).- 重複步驟 2, 當找不到一條增廣路的時候, 重複步驟 1, 重新建立層次圖, 直到從源點不能到達匯點爲止。
算法實現
#include <queue>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int MAXN = 510;
const int MAXN_INT = (1 << 29);
int n, m;
int dis[MAXN];
int mp[MAXN][MAXN];
int bfs(int s){
memset(dis, 0xff, sizeof(dis));
dis[s] = 0;
queue <int> que;
que.push(s);
while(!que.empty()){
int top = que.front();
que.pop();
for(int i = 1; i <= n; i++){
if(dis[i] < 0 && mp[top][i] > 0){
dis[i] = dis[top] + 1;
que.push(i);
}
}
}
if(dis[n] > 0) return true;
return false;
}
int Find(int x, int low){
int a = 0;
if(x == n) return low;
for(int i = 1; i <= n; i++){
if(mp[x][i] > 0
&& dis[i] == dis[x] + 1
&& (a = Find(i, min(low, mp[x][i])))){
mp[x][i] -= a;
mp[i][x] += a;
return a;
}
}
return 0;
}
int main(){
while(scanf("%d%d", &n, &m) != EOF){
memset(mp, 0, sizeof(mp));
int u, v, w;
for(int i = 0; i < m; i++){
scanf("%d%d%d", &u, &v, &w);
mp[u][v] += w;
}
int ans = 0, tmp;
while(bfs(1)){
while(tmp = Find(1, MAXN_INT))
ans += tmp;
}
printf("%d\n", ans);
}
return 0;
}
當前弧優化和多路增廣:#include <queue>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int MAXN = 101000;
const int MAXN_INT = (1 << 29);
struct Edge{
int v, w, nxt;
};
int s, t;
int n, m, ecnt;
Edge edge[MAXN * 2];
int head[MAXN], dis[MAXN], curEdge[MAXN];
void init(){
ecnt = 0;
memset(dis, -1, sizeof(dis));
memset(edge, 0, sizeof(edge));
memset(head, -1, sizeof(head));
}
void addEdge(int u, int v, int w){
edge[ecnt].v = v;
edge[ecnt].w = w;
edge[ecnt].nxt = head[u];
head[u] = ecnt++;
}
int bfs(){
dis[t] = 0;
queue <int> que;
que.push(t);
while(!que.empty()){
int u = que.front();
que.pop();
for(int i = head[u]; i + 1; i = edge[i].nxt){
if(dis[edge[i].v] == -1 && edge[i ^ 1].w > 0){
dis[edge[i].v] = dis[u] + 1;
que.push(edge[i].v);
}
}
}
return dis[s] != -1;
}
int dfs(int u, int v, int flow){
if(u == t) return flow;
int delta = flow;
for(int &i = curEdge[u]; i + 1; i = edge[i].nxt){
if(dis[u] == dis[edge[i].v] + 1 && edge[i].w){
int d = dfs(edge[i].v, v, min(delta, edge[i].w));
edge[i].w -= d, edge[i ^ 1].w += d;
delta -= d;
if(delta == 0) break;
}
}
return flow - delta;
}
int dinic(){
int ans = 0;
while(bfs()){
for(int i = 0; i < n; i++)
curEdge[i] = head[i];
ans += dfs(s, t, MAXN_INT);
}
return ans;
}
int main(){
while(scanf("%d%d", &n, &m) != EOF){
init();
int u, v, w;
for(int i = 0; i < m; i++){
scanf("%d%d%d", &u, &v, &w);
addEdge(u, v, w);
addEdge(v, u, 0);
}
printf("%d\n", dinic());
}
return 0;
}
時間複雜度
$O(V^2E)
最短增廣路算法(SAP)
算法思想
最短增廣路算法是一種運用距離標號使尋找增廣路的時間複雜度下降的算法。所謂的距離標號就是某個點到匯點的最少的弧的數量(即當邊權爲1時某個點的最短路徑長度). 設點i的標號爲d[i], 那麼如果將滿足d[i] = d[j] + 1, 且增廣時只走允許弧, 那麼就可以達到”怎麼走都是最短路”的效果. 每個點的初始標號可以在一開始用一次從匯點沿所有反向的BFS求出.
算法流程
1) 定義節點的標號爲到匯點的最短距離;
2) 每次沿可行邊進行增廣, 可行邊即: 假設有兩個點 i, j 若 d[i] = 3, d[j] = 4, 則d[j] = d[i] + 1, 也就是從 j 到 i 有一條邊.
3) 找到增廣路後,將路徑上所有邊的流量更新.
4) 遍歷完當前結點的可行邊後更新當前結點的標號爲d[now]=min(d[next]|Flow(now,next)>0)+1 ,使下次再搜的時候有路可走。
5) 圖中不存在增廣路後即退出程序,此時得到的流量值就是最大流。
需要注意的是, 標號的更新過程首先我們要理解更新標號的目的。標號如果需要更新,說明在當前的標號下已經沒有增廣路可以繼續走,這時更新標號就可以使得我們有繼續向下走的可能,並且每次找的都是能走到的點中標號最小的那個點,這樣也使得每次搜索長度最小.
下面的圖演示了標號的更新過程:
- 首先我們假設有個圖如下,爲了簡化沒有標箭頭也沒有寫流量:
- 爲圖標號, 每個點的標號爲其到匯點的最短距離(這裏把每條邊看作1)
- 第一遍遍歷時,找到了1->2->9這樣一條增廣路以後,更新邊上流量值, 得到下圖
棕色字體爲邊上的流量值。這時按照標號再搜一遍,發現從1出發已經找不到增廣路了,因爲flow(1,2)等於0不可以走,h[1]=2,h[3]=2≠h[1]+1,h[5]=4≠h[1]+1 , 所以這時更新1的標號, - 按照
min(h[next]|Flow(now,next)>0)+1 ,修改後h[1]=h[3]+1=3 .
- 第二遍遍歷以後找到了這樣一條增廣路:1->3->4->9,做完這條路以後又發現無法找到可行邊了,這時再更新標號使圖中有路可走,如上文所說的那樣做,再次修改後
h[1]=h[5]+1=5 ,就這樣搜索並更新直到變成下圖
- 這時再更新h[1]發現沒有點可以用來更新h[1]了,於是此時
h[1]=∞ ,使程序退出。
GAP 優化: 由於可行邊定義爲:
算法實現
#include <queue>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int MAXN = 5010;
const int MAXN_INT = (1 << 29);
struct Edge{
int v, w, nxt;
};
bool isFind;
int head[MAXN];
Edge edge[MAXN];
int dis[MAXN], gap[MAXN];
int n, m, ecnt, aug, maxFlow;
void init(){
ecnt = maxFlow = 0;
memset(gap, 0, sizeof(gap));
memset(dis, 0, sizeof(dis));
memset(edge, 0, sizeof(edge));
memset(head, -1, sizeof(head));
gap[0] = n;
}
void addEdge(int u, int v, int w){
edge[ecnt].v = v;
edge[ecnt].w = w;
edge[ecnt].nxt = head[u];
head[u] = ecnt++;
}
void Find(int s){
int dx, augc, minDis;
if(s == n){
isFind = true;
maxFlow += aug;
return;
}
augc = aug;
minDis = n - 1;
for(int i = head[i]; i + 1; i = edge[i].nxt){
if(edge[i].w > 0){
if(dis[s] == dis[edge[i].v] + 1){
aug = min(aug, edge[i].w);
Find(edge[i].v);
if(dis[1] >= n) return;
if(isFind){
dx = i;
break;
}
aug = augc;
}
minDis = min(minDis, dis[edge[i].v]);
}
}
if(!isFind){
gap[dis[s]]--;
if(gap[dis[s]] == 0) dis[1] = n;
dis[s] = minDis + 1;
gap[dis[s]]++;
}else{
edge[dx].w -= aug;
edge[dx ^ 1].w += aug;
}
}
int main(){
while(scanf("%d%d", &n, &m) != EOF){
init();
int u, v, w;
for(int i = 0; i < m; i++){
scanf("%d%d%d", &u, &v, &w);
addEdge(u, v, w);
addEdge(v, u, 0);
}
while(dis[1] < n){
isFind = 0;
aug = MAXN_INT;
Find(1);
}
cout << maxFlow << endl;
}
return 0;
}
時間複雜度
預流推進算法
預流推進算法是從一個預流出發對活躍頂點沿着允許弧進行流量增廣,每次增廣稱爲一次推進。在推進過程中,流一定滿足流量限制條件,但一般不滿足流量平衡條件, 因此只是一個僞流。此外, 如果一個僞流中, 從每個頂點(除源點 V s 、匯點 V t 外)流出的流量之和總是小於等於流入該頂點的流量之和, 稱這樣的僞流爲預流。因此這類算法被稱爲預流推進算法。
算法流程
- 首先用一邊 BFS 爲圖中每個頂點一個標號dis[v], 表示該點到v的最短路.
- 將與 S 相連的邊設爲滿流, 並將這時產生的活動結點加入隊列Q。
- 選出 Q 的一個活動結點 u 並依次判斷殘量網咯 G’ 中每條邊(u, v), 若
dis[u]=min(dis[v]+1) 則順着這些邊推流, 直到 Q 變成非活動結點(不存在多餘流量).- 如果 u 還是活動結點,則需要對 u 進行重新標號:
dis[u]=min(dis[v]+1) , 其中邊 (u, v) 存在於 G’ 中,然後再將 u 加入隊列。- 重複3, 4兩個步驟直到隊列 Q 爲空。
算法實現
const int size = 501;
const int MAX = 1 << 15;
int graph[size][size];
int label[size]; //標號
bool visited[size];
bool bfs(int st, int ed)
{
memset(label, -1, sizeof(label));
memset(visited, false, sizeof(visited));
label[st] = 0;
visited[st] = true;
vector < int >plist;
plist.push_back(st);
while (plist.size()) {
int p = plist[0];
plist.erase(plist.begin());
for (int i = 0; i < size; i++) {
if (graph[i][p] > 0 && !visited[i]) {
plist.push_back(i);
visited[i] = true;
label[i] = label[p] + 1;
}
}
}
if (label[ed] == -1) {
return false;
}
return true;
}
int inflow[size]; //流入量
int maxFlow()
{
memset(inflow, 0, sizeof(inflow));
//hights
bfs(size - 1, 0); //end point: size - 1, start point: 0
memset(visited, false, sizeof(visited));
//prepare()
vector < int >plist;
for (int i = 0; i < size; i++) {
if (graph[start][i] > 0) {
inflow[i] = graph[start][i];
graph[start][i] -= inflow[i];
graph[i][start] += inflow[i];
if (!visited[i]) {
plist.push_back(i);
visited[i] = true;
}
}
}
while (plist.size()) {
int p = plist[0];
plist.erase(plist.begin());
visited[p] = false;
int minLabel = -1;
for (int i = 0; i < size; i++) {
if (graph[p][i] > 0) {
if (label[p] == label[i] + 1) {
int flow = min(inflow[p], graph[p][i]);
inflow[p] -= flow;
inflow[i] += flow;
graph[p][i] -= flow;
graph[i][p] += flow;
if (!visited[i] && inflow[i] > 0) {
plist.push_back(i);
visited[i] = true;
}
}
}
}
if (inflow[p] > 0 && p != end) {
for (int i = 0; i < size; i++) {
if (graph[p][i] > 0) {
if (minLabel == -1 || minLabel > label[i] + 1) {
minLabel = label[i] + 1;
}
}
}
if (!visited[p] && minLabel != -1 && minLabel < size) //minLabel < size, 這個條件需要加上, 因爲經過測試發現有死循環的可能
{
for (int i = 0; i < size; i++) {
if (label[i] + 1 == minLabel && graph[p][i] > 0) {
visited[p] = true;
label[p] = minLabel;
plist.push_back(p);
break;
}
}
}
}
}
return inflow[end];
}
複雜度分析
如果該算法的Q是標準的FIFO隊列,則時間複雜度爲
最小費用最大流
簡介
最小費用最大流是解決這麼一種問題: 對於圖中的每一條邊來說, 除了有一個最大容量的屬性以外,還有一個費用屬性, 即流過這條邊的單位流量的花費。求解的問題爲在保證從源點到匯點的流量最大的前提下使得花費最少。
求解思想
我們來考慮這麼一個問題: 在最短路的一些變形的題目中往往有這種題,每條路不僅僅有一個長度還有一個建設的費用, 最終求從起點到終點在保證路最短的前提下,使得花費的錢最少。當時我們是怎麼求解的呢?
首先我們知道,最短路的長度是一定的,但是組成一條最短路的邊是不一定的,所以我們在搜索這條最短路的時候只要通過調整待選邊的優先級來控制搜索的方向就可以滿足上述問題的要求。
這個問題跟我們現在求解的最小費用最大流問題神似啊,只要我們在尋找增廣路的時候調整待選邊的優先級來控制尋找方向,這個問題就可以解決了啊。我們直到對於一條增廣路來說, 花費滿足:
求解算法
基於最大流的三種算法,求解最小費用最大流也具有三種算法,我們來對比一下這三對算法:
最大流 EK 算法: 每次用廣搜尋找一條最短的增廣路(即包含最少的邊),然後沿其增廣。
費用流 E’K’ 算法: 每次用spfa計算圖的距離標號,然後沿着可行邊進行增廣。最大流 DINIC 算法: 用廣搜獲得每個點到源點的距離標號,增廣時沿距離標號嚴格減1的路徑增廣,直到網絡中不再存在這麼一條路徑,那麼重新廣搜計算距離標號,如果廣搜發現整個源點到匯點已經不連通那麼退出算法。
費用流 原始對偶 算法: 用 SPFA 獲得每個點到源點的最短路,增廣時沿着最短路前進的方向增廣, 直到網絡中不存在一條路徑時重新 SPFA 求最短路, 直到沒有一條最短路可以到達匯點爲止。最大流 SAP 算法: 與 dinic 一樣基於距離標號,不過這裏保存的是到匯點的距離標號。並且考慮每次增廣對網絡的影響,發現增廣只會使點的距離標號變大,並且並不會破壞距離標號
dis[u]<=dis[v]+w[u,v] 的性質,只會使得等號不再成立。找不到可行邊就是因爲沒有一個結點v使得dis[u]==dis[v]+w[u,v] 。那麼重新使等號成立的方法也很簡單,並不需要重新計算整個圖的距離標號,只需要調整距離標號:如果從u點開始尋找增廣路沒有成功,即沒有一個v使得dis[u]==dis[v]+w[u,v] 那麼在所有(v∈V)中找到距離標號最小的一個v,使dis[u]=dis[v]+w[u,v] 即可。費用流 ZKW 算法: 每次增廣,同樣不會破壞距離標號dis[u]<=dis[v]+w[u,v] ,只會使得等號不再成立。並且被破壞的點並沒有很多(只有在增廣路上的點有可能被破壞)。因此並不需要SPFA來重新計算全部的距離標號。如果某一次尋找可行邊組成增廣路的嘗試進行到點u失敗,那麼在所有的邊$(v∈V中找到距離標號最小的一個v,使 dis[v] == dis[v] + w[u, v]&成立即可。
費用流 E’K’ 算法
思想上面說過了, 就是把最大流 EK 算法裏面的 bfs 替換爲 SPFA, 改變遍歷的優先級來實現:
算法步驟
與 EK 算法相同, 只不過將 bfs 換成 spfa求最短路, 邊權爲該邊的單位流量花費.
如下圖所示
算法實現
#include <queue>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int MAXN = 1010;
const int MAXM = 1000100;
const int MAXN_INT = (1 << 29);
struct Edge{
int v, w, c, nxt;
};
struct Node{
int id, v;
};
bool vis[MAXN];
Node pre[MAXN];
Edge edge[MAXN];
int n, m, ecnt, sumFlow;
int head[MAXN], dis[MAXN];
void init(){
ecnt = 0;
memset(edge, 0, sizeof(edge));
memset(head, -1, sizeof(head));
}
void addEdge(int u, int v, int c, int w){
edge[ecnt].v = v;
edge[ecnt].w = w;
edge[ecnt].c = c;
edge[ecnt].nxt = head[u];
head[u] = ecnt++;
}
bool SPFA(int s, int t, int n){
queue <int> que;
memset(vis, 0, sizeof(vis));
fill(dis, dis + MAXN, MAXN_INT);
vis[s] = true;
dis[s] = 0;
que.push(s);
while(!que.empty()){
int u =que.front();
que.pop();
vis[u] = false;
for(int i = head[u]; i + 1; i = edge[i].nxt){
int v = edge[i].v;
if(edge[i].c && dis[v] > dis[u] + edge[i].c){
dis[v] = dis[u] + edge[i].c;
pre[v].v = u;
pre[v].id = i;
if(!vis[v]){
que.push(v);
vis[v] = true;
}
}
}
}
if(dis[t] == MAXN_INT) return false;
return true;
}
int MCMF(int s, int t, int n){
int flow = 0;
int minCost = 0;
while(SPFA(s, t, n)){
int minFlow = MAXN_INT + 1;
for(int i = t; i != s; i = pre[i].v){
minFlow = min(minFlow, edge[pre[i].id].w);
}
for(int i = t; i != s; i = pre[i].v){
edge[pre[i].id].w -= minFlow;
edge[pre[i].id ^ 1].w += minFlow;
}
minCost += dis[t] * minFlow;
}
sumFlow = flow;
return minCost;
}
int main(){
while(scanf("%d%d", &n, &m) != EOF){
int u, v, c, w;
for(int i = 0; i < m; i++){
scanf("%d%d%d%d", &u, &v, &c, &w);
addEdge(u, v, c, w);
addEdge(v, u, -c, 0);
}
int ans = MCMF(1, n, n);
printf("%d\n", ans);
}
return 0;
}
第二部分轉自:(https://blog.andrewei.info/2016/04/11/network-flows/)