【學習筆記】網絡流算法簡單入門
【大前言】
網絡流是一種神奇的問題,在不同的題中你會發現各種各樣的神仙操作。
而且從理論上講,網絡流可以處理所有二分圖問題。
二分圖和網絡流的難度都在於問題建模,一般不會特意去卡算法效率,所以只需要背一兩個簡單算法的模板就能應付大部分題目了。
一:【基本概念及性質】
【網絡流基本概念】
-
網絡流 \((NetWork\) \(Flow)\) : 一種類比水流的解決問題的方法。
(下述概念均會用水流進行解釋) -
網絡 \((NetWork)\) : 可以理解為擁有源點和匯點的有向圖。
(運輸水流的水管路線路) -
弧 \((arc)\) : 可以理解為有向邊。下文均用 “邊” 表示。
(水管) -
弧的流量 \((Flow)\) : 簡稱流量。在一個流量網絡中每條邊都會有一個流量,表示為 \(f(x,y)\) ,根據流函數 \(f\) 的定義,\(f(x,y)\) 可為負。
(運輸的水流量) -
弧的容量 \((Capacity)\) : 簡稱容量。在一個容量網絡中每條邊都會有一個容量,表示為 \(c(x,y)\) 。
(水管規格。即可承受的最大水流量) -
源點 \((Sources)\) : 可以理解為起點。它會源源不斷地放出流量,表示為 \(S\) 。
(可無限出水的 \(NB\) 水廠) -
匯點 \((Sinks)\) : 可以理解為終點。它會無限地接受流量,表示為 \(T\) 。
(可無限收集水的 \(NB\) 小區) -
容量網絡: 擁有源點和匯點且每條邊都給出了容量的網絡。
(安排好了水廠,小區和水管規格的路線圖) -
流量網絡: 擁有源點和匯點且每條邊都給出了流量的網絡。
(分配好了各個水管水流量的路線圖) -
弧的殘留容量: 簡稱殘留容量。在一個殘量網絡中每條邊都會有一個殘留容量 。對於每條邊,殘留容量 \(=\) 容量 \(-\) 流量。初始的殘量網絡即為容量網絡。
(表示水管分配了水流量后還能繼續承受的水流量) -
殘量網絡: 擁有源點和匯點且每條邊都有殘留容量的網絡。殘量網絡 \(=\) 容量網絡 \(-\) 流量網絡。
(表示了分配了一定的水流量后還能繼續承受的水流量路線圖)
關於流量,容量,殘留容量的理解見下圖:
(用 \(c\) 表示容量,\(f\) 表示流量,\(flow\) 表示殘留容量)
【網絡流三大性質】
-
容量限制: \(\forall (x,y) \in E,f(x,y) \leqslant c(x,y)\) 。
(如果水流量超過了水管規格就爆了呀) -
流量守恆: \(\forall x \in V且x \ne S且x \ne T,\sum_{(u,x) \in E}f(u,x) = \sum_{(x,v) \in E}f(x,v)\) 。
(對於所有的水管交界處,有多少水流量過來,就應有多少水流量出去,保證水管質量良好不會泄露並且不會無中生有) -
斜對稱性: \(\forall (x,y) \in E,f(y,x)=-f(x,y)\) 。
(可以暫且感性理解為矢量的正負。在網絡流問題中,這是非常重要的一個性質)
還有一些其他的概念和性質將在后面補充。
二:【最大流】
1.【概念補充】
-
網絡的流量: 在某種方案下形成的流量網絡中匯點接收到的流量值。
(小區最終接收到的總水流量) -
最大流: 網絡的流量的最大值。
(小區最多可接受到的水流量) -
最大流網絡: 達到最大流的流量網絡。
(使得小區接收到最多水流量的分配方案路線圖)
2.【增廣路算法 ( EK )】
【概念補充】
-
增廣路 \((Augmenting\) \(Path)\): 一條在殘量網絡中從 \(S\) 到 \(T\) 的路徑,路徑上所有邊的殘留容量都為正。
(可以成功從水廠將水送到小區的一條路線) -
增廣路定理 \((Augmenting\) \(Path\) \(Theorem)\): 流量網絡達到最大流當且僅當殘量網絡中沒有增廣路。
(無法再找到一路線使得小區獲得更多的流量了) -
增廣路方法 \((Ford-Fulkerson)\): 不斷地在殘量網絡中找出一條從 \(S\) 到 \(T\) 的增廣路,然后根據木桶定律向匯點發送流量並修改路徑上的所有邊的殘留容量,直到無法找到增廣路為止。該方法的基礎為增廣路定理,簡稱 \(FF\) 方法。
(如果有一條路徑可以將水運到小區里就執行,直到無法再運送時終止) -
增廣路算法 \((Edmonds-Karp)\): 基於增廣路方法的一種算法,核心為 \(bfs\) 找最短增廣路,並按照 \(FF\) 方法執行操作。增廣路算法的出現使得最大流問題被成功解決,簡稱 \(EK\) 算法。
【算法流程】
下面對 \(EK\) 算法作詳細介紹。
\((1).\) 用 \(bfs\) 找到任意一條經過邊數最少的最短增廣路,並記錄路徑上各邊殘留容量的最小值 \(cyf\)(殘\(c\) 余\(y\) \(flow\))。 (木桶定律。眾多水管一個也不能爆,如果讓最小的剛好不會爆,其它的也就安全了)
\((2).\) 根據 \(cyf\) 更新路徑上邊及其反向邊的殘留容量值。答案(最大流)加上 \(cyf\) 。
\((3).\) 重復 \((1),(2)\) 直至找不到增廣路為止。
對於 \((2)\) 中的更新操作,利用鏈表的特性,從 \(2\) 開始存儲,那么 \(3\) 與 \(2\) 就互為一對反向邊,\(5\) 與 \(4\) 也互為一對反向邊 \(......\)
只需要記錄增廣路上的每一條邊在鏈表中的位置下標,然后取出來之后用下標對 \(1\) 取異或就能快速得到它的反向邊。
【算法理解】
關於建圖:
在具體實現中,由於增廣路是在殘量網絡中跑的,所以只需要用一個變量 \(flow\) 記錄殘留容量就足夠了,容量和流量一般不記錄。
為了保證算法的最優性(即網絡的流量要最大),可能在為某一條邊分配了流量后需要反悔,所以要建反向邊。在原圖中,正向邊的殘留容量初始化為容量,反向邊的殘留容量初始化為 \(0\)(可理解為反向邊容量為 \(0\))。
當我們將邊 \((x,y)\)(在原圖中可能為正向也可能為反向)的殘留容量 \(flow\) 用去了 \(F\) 時,其流量增加了 \(F\),殘留容量 \(flow\) 應減少 \(F\)。根據斜對稱性,它的反邊 \((y,x)\) 流量增加了 \(-F\),殘留容量 \(flow'\) 應減去 \(-F\)(即加上 \(F\))。
那么如果在以后找增廣路時選擇了這一條邊,就等價於:將之前流出去的流量的一部分(或者全部)反悔掉了個頭,跟隨着新的路徑流向了其它地方,而新的路徑上在到達這條邊之前所積蓄的流量 以及 之前掉頭掉剩下的流量 則順着之前的路徑流了下去。
同理,當使用了反向邊 \((y,x)\) 的殘留容量時也應是一樣的操作。
還是之前那個圖,下面是找到了一條最短增廣路 \(1 → 3 → 2 → 4\)(其中三條邊均為黑邊)后的情況:
(不再顯示容量和流量,用 \(flow\) 表示殘留容量,灰色邊表示原圖上的反向邊,藍色小水滴表示水流量)
然后是第二條最短增廣路 \(1 → 7 → 6 → 2 \dashrightarrow 3 → 8 → 5 → 4\)(其中 \(f(2,3)\) 為灰邊,其余均為黑邊,紫色小水滴表示第二次新增的水流量):
注:由於在大部分題目中都不會直接使用容量和流量,所以通常會直接說某某之間連一條流量為某某的邊,在沒有特別說明的情況下,其要表示的含義就是殘留容量。后面亦不再強調“殘留”,直接使用“流量”。
【時間復雜度分析】
每條邊最多會被增廣 \(O(\frac{n}{2}-1)\) 次(證明),一共 \(m\) 條邊,總增廣次數為 \(nm\) 。
一次 \(bfs\) 增廣最壞是 \(O(m)\) 的,\(bfs\) 之后更新路徑上的信息最壞為 \(O(n)\)(可忽略)。
最壞時間復雜度為:\(O(nm^2)\) 。
實際應用中效率較高,一般可解決 \(10^4\) 以內的問題。
【Code】
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<queue>
#define Re register int
using namespace std;
const int N=1e4+3,M=1e5+3,inf=2e9;
int x,y,z,o=1,n,m,h,t,st,ed,maxflow,Q[N],cyf[N],pan[N],pre[N],head[N];
struct QAQ{int to,next,flow;}a[M<<1];
inline void in(Re &x){
int f=0;x=0;char c=getchar();
while(c<'0'||c>'9')f|=c=='-',c=getchar();
while(c>='0'&&c<='9')x=(x<<1)+(x<<3)+(c^48),c=getchar();
x=f?-x:x;
}
inline void add(Re x,Re y,Re z){a[++o].flow=z,a[o].to=y,a[o].next=head[x],head[x]=o;}
inline int bfs(Re st,Re ed){
for(Re i=0;i<=n;++i)pan[i]=0;
h=1,t=0,pan[st]=1,Q[++t]=st,cyf[st]=inf;//注意起點cfy的初始化
while(h<=t){
Re x=Q[h++];
for(Re i=head[x],to;i;i=a[i].next)
if(a[i].flow&&!pan[to=a[i].to]){//增廣路上的每條邊殘留容量均為正
cyf[to]=min(cyf[x],a[i].flow);
//用cyf[x]表示找到的路徑上從S到x途徑邊殘留容量最小值
Q[++t]=to,pre[to]=i,pan[to]=1;//記錄選擇的邊在鏈表中的下標
if(to==ed)return 1;//如果達到終點,說明最短增廣路已找到,結束bfs
}
}
return 0;
}
inline void EK(Re st,Re ed){
while(bfs(st,ed)==1){
Re x=ed;maxflow+=cyf[ed];//cyf[ed]即為當前路徑上邊殘留容量最小值
while(x!=st){//從終點開始一直更新到起點
Re i=pre[x];
a[i].flow-=cyf[ed];
a[i^1].flow+=cyf[ed];
x=a[i^1].to;//鏈表特性,反向邊指向的地方就是當前位置的父親
}
}
}
int main(){
in(n),in(m),in(st),in(ed);
while(m--)in(x),in(y),in(z),add(x,y,z),add(y,x,0);
EK(st,ed);
printf("%d",maxflow);
}
3.【Dinic】
在 \(EK\) 算法中,每一次 \(bfs\) 最壞可能會遍歷整個殘量網絡,但都只會找出一條最短增廣路。
那么如果一次 \(bfs\) 能夠找到多條最短增廣路,速度嗖~嗖~地就上去了。
\(Dinic\) 算法便提供了該思路的一種實現方法。
網絡流的算法多且雜,對於初學者來說,在保證效率的前提下優化\(Dinic\)應該是最好寫的一種了。
【算法流程】
\((1).\) 根據 \(bfs\) 的特性,找到 \(S\) 到每個點的最短路徑(經過最少的邊的路徑),根據路徑長度對殘量網絡進行分層,給每個節點都給予一個層次,得到一張分層圖。
\((2).\) 根據層次反復 \(dfs\) 遍歷殘量網絡,一次 \(dfs\) 找到一條增廣路並更新,直至跑完能以當前層次到達 \(T\) 的所有路徑。
【多路增廣】
可以發現,一次 \(bfs\) 會找到 \([1,m]\) 條增廣路,大大減少了 \(bfs\) 次數,但 \(dfs\) 更新路徑上的信息仍是在一條一條地進行,效率相較於 \(EK\) 並沒有多大變化。
為了做到真正地多路增廣,還需要進行優化。
在 \(dfs\) 時對於每一個點 \(x\),記錄一下 \(x \rightsquigarrow T\) 的路徑上往后走已經用掉的流量,如果已經達到可用的上限則不再遍歷 \(x\) 的其他邊,返回在 \(x\) 這里往后所用掉的流量,回溯更新 \(S \rightsquigarrow x\) 上的信息。
如果到達匯點則返回收到的流量,回溯更新 \(S \rightsquigarrow T\) 上的信息。
【當前弧優化】
原理:在一個分層圖當中,\(\forall x \in V\),任意一條從 \(x\) 出發處理結束的邊(弧),都成了 “廢邊”,在下一次到達 \(x\) 時不會再次使用。
(水管空間已經被榨干凈了,無法再通過更多的水流,直接跳過對這些邊的無用遍歷)
實現方法:用數組 \(cur[x]\) 表示上一次處理 \(x\) 時遍歷的最后一條邊(即 \(x\) 的當前弧),其使用方法與鏈表中的 \(head\) 相同,只是 \(cur\) 會隨着圖的遍歷不斷更新。由於大前提是在一個分層圖當中,所以每一次 \(bfs\) 分層后都要將 \(cur\) 初始化成 \(head\) 。
特別的,在稠密圖中最能體現當前弧優化的強大。
【時間復雜度分析】
最壞時間復雜度為:\(O(n^2m)\)。(看不懂的證明)
(特別的,對於二分圖,\(Dinic\) 最壞時間復雜度為 \(n\sqrt{m}\))
實際應用中效率較高,一般可解決 \(10^5\) 以內的問題。
【Code】
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<queue>
#define Re register int
using namespace std;
const int N=1e4+3,M=1e5+3,inf=2147483647;
int x,y,z,o=1,n,m,h,t,st,ed,Q[N],cur[N],dis[N],head[N];long long maxflow;
struct QAQ{int to,next,flow;}a[M<<1];
inline void in(Re &x){
int f=0;x=0;char c=getchar();
while(c<'0'||c>'9')f|=c=='-',c=getchar();
while(c>='0'&&c<='9')x=(x<<1)+(x<<3)+(c^48),c=getchar();
x=f?-x:x;
}
inline void add(Re x,Re y,Re z){a[++o].flow=z,a[o].to=y,a[o].next=head[x],head[x]=o;}
inline int bfs(Re st,Re ed){//bfs求源點到所有點的最短路
for(Re i=0;i<=n;++i)cur[i]=head[i],dis[i]=0;//當前弧優化cur=head
h=1,t=0,dis[st]=1,Q[++t]=st;
while(h<=t){
Re x=Q[h++],to;
for(Re i=head[x];i;i=a[i].next)
if(a[i].flow&&!dis[to=a[i].to]){
dis[to]=dis[x]+1,Q[++t]=to;
if(to==ed)return 1;
}
}
return 0;
}
inline int dfs(Re x,Re flow){//flow為剩下可用的流量
if(!flow||x==ed)return flow;//發現沒有流了或者到達終點即可返回
Re tmp=0,to,f;
for(Re i=cur[x];i;i=a[i].next){
cur[x]=i;//當前弧優化cur=i
if(dis[to=a[i].to]==dis[x]+1&&(f=dfs(to,min(flow-tmp,a[i].flow)))){
//若邊權為0,不滿足增廣路性質,或者跑下去無法到達匯點,dfs返回值f都為0,不必執行下面了
a[i].flow-=f,a[i^1].flow+=f;
tmp+=f;//記錄終點已經從x這里獲得了多少流
if(!(flow-tmp))break;
//1. 從st出來流到x的所有流被榨干。后面的邊都不用管了,break掉。
//而此時邊i很可能還沒有被榨干,所以cur[x]即為i。
//2. 下面兒子的容量先被榨干。不會break,但邊i成了廢邊。
//於是開始榨x的下一條邊i',同時cur[x]被更新成下一條邊i'
//直至榨干從x上面送下來的水流結束(即情況1)。
}
}
return tmp;
}
inline void Dinic(Re st,Re ed){
Re flow=0;
while(bfs(st,ed))maxflow+=dfs(st,inf);
}
int main(){
in(n),in(m),in(st),in(ed);
while(m--)in(x),in(y),in(z),add(x,y,z),add(y,x,0);
Dinic(st,ed);
printf("%lld",maxflow);
}
4.【ISAP】
\(To\) \(be\) \(continued...\)
5.【HLPP】
神奇的預流推進。。。
\(To\) \(be\) \(continued...\)
4.【算法效率測試】
因為網絡流算法的時間復雜度都不太好分析,所以用實際的題目來感受一下。
【測試一】
參測算法:
\((1).EK:\)
\((2).Dinic\) \(+\) 多路增廣(喵?喵?喵?居然卡 \(Dinic\)!)\(:\)
\((3).Dinic\) \(+\) 多路增廣 \(+\) 當前弧優化 \(:\)
【測試二】
參測算法:
\((1).EK:\)
\((2).Dinic\) \(+\) 多路增廣 \(+\) 當前弧優化 \(:\)
\((3).\) 匈牙利算法(\(QAQ\) 好像混入了奇怪的東西)\(:\)
\(To\) \(be\) \(continued...\)
5.【例題】
-
【模板】網絡最大流 \([P3376]\) \([Loj101]\)
【標簽】網絡流/最大流 -
【模板】二分圖匹配 \([P3386]\)
【標簽】二分圖匹配/網絡流/最大流 -
【網絡流 \(24\) 題】魔術球問題 \([P2765]\) \([Loj6003]\)
【標簽】貪心/網絡流/最大流/鬼畜建圖 -
酒店之王 \([P1402]\)
教輔的組成 \([P1231]\)
吃飯 \(Dining\) \([P2891]\) \([Bzoj1711]\) \([Poj3281]\)
【標簽】二分圖匹配/網絡流/最大流/套路題
三:【有上下界的最大流】
\(To\) \(be\) \(continued...\)
四:【最小割】
1.【概念補充】
-
網絡的割集\((Network\) \(Cut\) \(Set)\) : 把一個源點為 \(S\),匯點為 \(T\) 的網絡中的所有點划分成兩個點集 \(s\) 和 \(t\),\(S \in s,T \in t\),由 \(x \in s\) 連向 \(y \in t\) 的邊的集合稱為割集。可簡單理解為:對於一個源點為 \(S\),匯點為 \(T\) 的網絡,若刪除一個邊集 \(E’ \subseteq E\) 后可以使 \(S\) 與 \(T\) 不連通,則成 \(E’\) 為該網絡的一個割集。
(有壞人不想讓小區通水,用鋸子割掉了一些邊) -
最小割 \((Minimum\) \(Cut)\) : 在一個網絡中,使得邊容量之和最小的割集。
(水管越大越難割,壞人想要最節省力氣的方案) -
最大流最小割定理:\((Maximum\) \(Flow,Minimum\) \(Cut\) \(Theorem)\) 任意一個網絡中的最大流等於最小割。【證明】
(可以感性理解為:最大流網絡中一定是找出了所有的可行路徑,將每條路徑上都割掉一條邊就能保證 \(S,T\) 一定不連通,在此前提下每條路徑上都割最小的邊,其等價於最大流)
2.【最大權閉合子圖】
\(To\) \(be\) \(continued...\)
3.【例題】
- 【網絡流 \(24\) 題】方格取數問題 \([P2774]\) \([Loj6007]\)
【標簽】網絡流/最大流/最小割/套路題
六:【費用流】
1.【概念補充】
-
單位流量的費用 \((Cost)\) : 簡稱單位費用。顧名思義,一條邊的費用 \(=\) 流量 \(\times\) 單位費用。表示為 \(w(x,y)\) 。
(每根水管運一份水的花費。與\(“\)殘留容量\(”\)的簡化類似,通常直接稱\(“\)費用\(”\)) -
最小費用最大流: 在最大流網絡中,使得總費用最小。
(在運最多水的前提下,花錢最少)
2.【EK】
【算法流程】
只需將最大流 \(EK\) 算法中的流程 \((1)\) \(“\) \(bfs\) 找到任意一條最短增廣路 \(”\) 改為 \(“\) \(Spfa\) 找到任意一條單位費用之和最小的增廣路 \(”\),即可得到最小費用最大流。
特別的,為了提供反悔機會,原圖中 \(\forall (x,y) \in E\) 的反向邊單位費用應為 \(-w(x,y)\) 。(為什么不用 \(dijkstra\)?原因就在這里啊!)
【Code】
#include<algorithm>
#include<cstdio>
#include<queue>
#define LL long long
#define Re register int
using namespace std;
const int N=5003,M=5e4+3,inf=2e9;
int x,y,z,w,o=1,n,m,h,t,st,ed,cyf[N],pan[N],pre[N],dis[N],head[N];LL mincost,maxflow;
struct QAQ{int w,to,next,flow;}a[M<<1];queue<int>Q;
inline void in(Re &x){
int f=0;x=0;char c=getchar();
while(c<'0'||c>'9')f|=c=='-',c=getchar();
while(c>='0'&&c<='9')x=(x<<1)+(x<<3)+(c^48),c=getchar();
x=f?-x:x;
}
inline void add(Re x,Re y,Re z,Re w){a[++o].flow=z,a[o].w=w,a[o].to=y,a[o].next=head[x],head[x]=o;}
inline void add_(Re a,Re b,Re flow,Re w){add(a,b,flow,w),add(b,a,0,-w);}
inline int SPFA(Re st,Re ed){
for(Re i=0;i<=ed;++i)dis[i]=inf,pan[i]=0;
Q.push(st),pan[st]=1,dis[st]=0,cyf[st]=inf;
while(!Q.empty()){
Re x=Q.front();Q.pop();pan[x]=0;
for(Re i=head[x],to;i;i=a[i].next)
if(a[i].flow&&dis[to=a[i].to]>dis[x]+a[i].w){
dis[to]=dis[x]+a[i].w,pre[to]=i;
cyf[to]=min(cyf[x],a[i].flow);
if(!pan[to])pan[to]=1,Q.push(to);
}
}
return dis[ed]!=inf;
}
inline void EK(Re st,Re ed){
while(SPFA(st,ed)){
Re x=ed;maxflow+=cyf[ed],mincost+=(LL)cyf[ed]*dis[ed];
while(x!=st){//和最大流一樣的更新
Re i=pre[x];
a[i].flow-=cyf[ed];
a[i^1].flow+=cyf[ed];
x=a[i^1].to;
}
}
}
int main(){
in(n),in(m),in(st),in(ed);
while(m--)in(x),in(y),in(z),in(w),add_(x,y,z,w);
EK(st,ed);
printf("%lld %lld",maxflow,mincost);
}
3.【Primal-Dual】
\(To\) \(be\) \(continued...\)
4.【ZKW 算法】
\(To\) \(be\) \(continued...\)
5.【算法效率測試】
\(To\) \(be\) \(continued...\)
6.【例題】
-
【模板】最小費用最大流 \([P3381]\)
【標簽】網絡流/費用流 -
【網絡流 \(24\) 題】負載平衡問題 \([P4016]\) \([Loj6013]\)
【標簽】網絡流/費用流/鬼畜建圖
七:【常見問題模型】
\(To\) \(be\) \(continued...\)