網路流詳解+題目
介紹
首先先了解網路和流
再瞭解一下網路流的相關定義:
- 源點:有n個點,有m條有向邊,有一個點很特殊,只出不進,叫做源點。
- 匯點:另一個點也很特殊,只進不出,叫做匯點。
- 容量和流量:每條有向邊上有兩個量,容量和流量,從i到j的容量通常用c[i,j]表示,流量則通常是f[i,j].
- 殘量:就是當前容量減去流量
網路流的常見問題
網路流問題中常見的有以下三種:最大流,最小割,費用流。
最大流
1、我們有一張圖,要求從源點流向匯點的最大流量(可以有很多條路到達匯點),就是我們的最大流問題。
最小費用最大流
2、最小費用最大流問題是這樣的:每條邊都有一個費用,代表單位流量流過這條邊的開銷。我們要在求出最大流的同時,要求花費的費用最小。
最小割
3、割其實就是刪邊的意思,當然最小割就是割掉 \(X\) 條邊來讓 \(S\) 跟 \(T\) 兩個集合(可以理解為 源點\(S\) 和 匯點\(T\) 不連通)不互通。我們要求 \(X\) 條邊加起來的流量總和最小。這就是最小割問題。
1. 求最大流
首先,明確最大流是幹什麼的
給定指定的一個有向圖,其中有兩個特殊的點源S(Sources)和匯T(Sinks),每條邊有指定的容量(Capacity),求滿足條件的從S到T的最大流(MaxFlow).
通俗一點,
就好比你家是匯 自來水廠是源
然後自來水廠和你家之間修了很多條水管子接在一起 水管子規格不一 有的容量大 有的容量小
然後問自來水廠開閘放水 你家收到水的最大流量是多少
如果自來水廠停水了 你家那的流量就是0 當然不是最大的流量
但是你給自來水廠交了100w美金 自來水廠拼命水管裡通水 但是你家的流量也就那麼多不變了 這時就達到了最大流
理解起來還好吧,也就是上文說的那樣
1.1Edmond-Karp演算法
1.1.1 演算法解析
首先引入增廣路的概念:
- 增廣路
增廣路指從s到t的一條路,水流流過這條路,使得當前可以到達t的流量可以增加。
通俗一點
在原圖 \(G\) 中若一條從源點到匯點的路徑上所有邊的 剩餘容量都大於 \(0\),這條路被稱為增廣路(Augmenting Path)。
通過定義,我們顯然可以看出求最大流其實就是不斷尋找增廣路的過程。
可以用BFS也可以用DFS,只是BFS快一點
詳細一點:就是用 BFS 找增廣路,然後對其進行增廣。你可能會問,怎麼找?怎麼增廣?
1、找?我們就從源點一直 BFS 走來走去,碰到匯點就停,然後增廣(每一條路都要增廣)。我們在 BFS 的時候就注意一下流量合不合法就可以了。
2、增廣?其實就是按照我們找的增廣路在重新走一遍。走的時候把這條路的能夠成的最大流量減一減,然後給答案加上最小流量就可以了。
再講一下 反向邊。增廣的時候要注意建造反向邊,原因是這條路不一定是最優的,這樣子程式可以進行反悔(就相當於藍色和橙色的線抵消了。)。假如我們對這條路進行增廣了,那麼其中的每一條邊的正向邊就減去流量,反向邊的流量就加上它的流量。
1.1.2 複雜度
EK 演算法的時間複雜度為 \(O(nm^2)\) (其中 \(n\) 為點數, \(m\) 為邊數)。效率還有很大提升空間。
1.1.3 模板
#include <bits/stdc++.h>
using namespace std;
int cnt=1;
int n,m,s,t;
int u,v,w;
bool vis[205];
int head[205];
struct o{
int pre,edge;
}p[10005];
struct node{
int next,to,w;
}e[10005];
void add(int x,int y,int w){
e[++cnt].w=w;
e[cnt].to=y;
e[cnt].next=head[x];
head[x]=cnt;
}
bool dfs(){
queue<int>q;
memset(vis,0,sizeof(vis));
q.push(s);
vis[s]=1;
while (!q.empty()){
int top=q.front();
q.pop();
for (int i=head[top];i;i=e[i].next){
if (!vis[e[i].to]&&e[i].w){//若沒去過且容量大於0
vis[e[i].to]=1;//標記去過
p[e[i].to].edge=i;//記錄路徑,edge為邊
p[e[i].to].pre=top;//記錄路徑,pre為上一個點
if (e[i].to==t){
return 1;
}q.push(e[i].to);
}
}
}return 0;//若全部搜完還沒有出現增廣路,返回0
}
int main(){
scanf("%d%d%d%d",&n,&m,&s,&t);
for (int i=1;i<=m;i++){
scanf("%d%d%d",&u,&v,&w);
add(u,v,w);
add(v,u,0);
}
long long ans=0;
while (dfs()){
int minn=999999999;
for (int i=t;i!=s;i=p[i].pre) //找最小容量
minn=min(minn,e[p[i].edge].w);
for (int i=t;i!=s;i=p[i].pre){//更改
e[p[i].edge].w-=minn;
e[p[i].edge^1].w+=minn;
}
ans+=minn;
}
printf("%lld",ans);
return 0;
}
1.2 Dinic演算法
1.2.1 演算法思路
Dinic演算法的思想也是分階段地在層次網路中增廣。它與最短增廣路演算法不同之處是:最短增廣路每個階段執行完一次BFS增廣後,要重新啟動BFS從源點Vs開始尋找另一條增廣路;而在Dinic演算法中,只需一次DFS過程就可以實現多次增廣,這是Dinic演算法的巧妙之處。Dinic演算法具體步驟如下:
(1)初始化容量網路和網路流。
(2)構造殘留網路和層次網路,若匯點不再層次網路中,則演算法結束。
(3)在層次網路中用一次DFS過程進行增廣,DFS執行完畢,該階段的增廣也執行完畢。
(4)轉步驟(2)。
在Dinic的演算法步驟中,只有第(3)步與最短增廣路相同。在下面例項中,將會發現DFS過程將會使演算法的效率有非常大的提高。
構造層次網路就是給圖分層,使得增廣路最短
1.2.2 複雜度
因為一次DFS的複雜度為 \(O(nm)\) ,所以,Dinic演算法的總複雜度即\(O(n^2m)\)。
1.2.3 模板
還是以模板題為例
#include <bits/stdc++.h>
#define LL long long
using namespace std;
int cnt=1;
int n,m,s,t;
int u,v,w;
bool vis[205];
int head[205];
int d[205];
struct node{
int next,to,w;
}e[10005];
LL ans=0;
inline int read(){
int x=0,f=1;
char ch=getchar();
while(ch<'0'||ch>'9'){
if(ch=='-')
f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9'){
x=x*10+ch-'0';
ch=getchar();
}
return x*f;
}
void add(int x,int y,int w){
e[++cnt].w=w;
e[cnt].to=y;
e[cnt].next=head[x];
head[x]=cnt;
}
bool bfs(){
memset(d,0x3f,sizeof(d));
memset(vis,0,sizeof(vis));
queue<int>q;
vis[s]=1;
d[s]=0;
q.push(s);
while (!q.empty()){
int top=q.front();
q.pop();
for (int i=head[top];i;i=e[i].next){
if (d[e[i].to]>d[top]+1&&e[i].w){
d[e[i].to]=d[top]+1;
if (!vis[e[i].to]){
vis[e[i].to]=1;
q.push(e[i].to);
}
}
}
}
if (d[t]==d[0]){
return 0;
}
return 1;
}
LL dfs(int x,int minn){
int use=0;
if (x==t){
ans+=minn;
return minn;
}
for (int i=head[x];i;i=e[i].next){
if (e[i].w&&d[e[i].to]==d[x]+1){
int nex=dfs(e[i].to,min(minn-use,e[i].w));
if (nex>0){
use+=nex;
e[i].w-=nex;
e[i^1].w+=nex;
if (use==minn)
break;
}
}
}
return use;
}
int main(){
n=read();m=read();s=read();t=read();
for (int i=1;i<=m;i++){
u=read();v=read();w=read();
add(u,v,w);
add(v,u,0);
}
while (bfs()){
dfs(s,999999999);
}
printf("%lld",ans);
return 0;
}
然後我們就會發現,我們TLE了一個點,這是就需要優化,可以想到至高無上的弧優化。
1.3 Dinic演算法+當前弧優化
1.3.1 演算法優化
當前弧優化實際上只是增加了一個數組 \(cur\),用 \(cur_i\) 代替鄰接表中的 \(head_i\) 。
原理是:當我們在dfs時,從u點出發訪問到了第i條邊,就說明前i-1條邊都已經被榨乾了,所以才到了第i條邊,因此我們直接改cur[u] = i,下次訪問從u點出發的邊時,就會直接訪問第i條邊,省略了前面i-1條邊。
不懂的話看看下面程式碼模板,就會理解了(或者可以去了解一下鏈式前向星)。
1.3.2 完整程式碼
#include <bits/stdc++.h>
#define LL long long
using namespace std;
int cnt=1;
int n,m,s,t;
int u,v,w;
bool vis[205];
int head[205];
int d[205];
int cur[205];
struct node{
int next,to,w;
}e[10005];
LL ans=0;
inline int read(){
int x=0,f=1;
char ch=getchar();
while(ch<'0'||ch>'9'){
if(ch=='-')
f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9'){
x=x*10+ch-'0';
ch=getchar();
}
return x*f;
}
void add(int x,int y,int w){
e[++cnt].w=w;
e[cnt].to=y;
e[cnt].next=head[x];
head[x]=cnt;
}
bool bfs(){
for (int i=0;i<=n;i++){
d[i]=0x3ffffff;
vis[i]=0;
cur[i]=head[i];
}
queue<int>q;
vis[s]=1;
d[s]=0;
q.push(s);
while (!q.empty()){
int top=q.front();
q.pop();
for (int i=head[top];i;i=e[i].next){
if (d[e[i].to]>d[top]+1&&e[i].w){
d[e[i].to]=d[top]+1;
if (!vis[e[i].to]){
vis[e[i].to]=1;
q.push(e[i].to);
}
}
}
}
if (d[t]==d[0]){
return 0;
}
return 1;
}
LL dfs(int x,int minn){
int use=0;
if (x==t){
ans+=minn;
return minn;
}
for (int i=head[x];i;i=e[i].next){
cur[x]=i;
if (e[i].w&&d[e[i].to]==d[x]+1){
int nex=dfs(e[i].to,min(minn-use,e[i].w));
if (nex>0){
use+=nex;
e[i].w-=nex;
e[i^1].w+=nex;
if (use==minn)
break;
}
}
}
return use;
}
int main(){
n=read();m=read();s=read();t=read();
for (int i=1;i<=m;i++){
u=read();v=read();w=read();
add(u,v,w);
add(v,u,0);
}
while (bfs()){
dfs(s,0x3ffffff);
}
printf("%lld",ans);
return 0;
}
2.求最小割
割\((CUT)\)
割是網路中頂點的劃分,它把對於一個網路流圖 \(G=(V,E)\)的所有頂點劃分成兩個集合 \(S\) 和 \(T\),且\(S+T=V\) ,其中源點\(s∈S\),匯點\(t∈T\)。記為\(c(S,T)\)。
定義割\((S,T)\)的容量\(c(S,T)\)表示所有從\(S\)到\(T\)的邊的容量之和(注意是有方向的),即\(c(S,T)=\sum\limits_{u\in S, v \in T}c(u,v)\)
就是一個恐怖分子想要,砍斷水管使你家斷水,至少要砍斷那些水管使水管容量和最小
顯然上圖的最小流是8,但是一想,最大流也是8,這之間有什麼關係嗎?
2.1 最大流最小割定理
在任何的網路中,最大流的值等於最小割的容量
具體的證明分三部分
1.任意一個流都小於等於任意一個割
這個很好理解 自來水公司隨便給你家通點水,構成一個流
恐怖分子隨便砍幾刀 砍出一個割
由於容量限制,每一根的被砍的水管子流出的水流量都小於管子的容量
每一根被砍的水管的水本來都要到你家的,現在流到外面 加起來得到的流量還是等於原來的流
管子的容量加起來就是割,所以流小於等於割
由於上面的流和割都是任意構造的,所以任意一個流小於任意一個割
2.構造出一個流等於一個割
當達到最大流時,根據增廣路定理
殘留網路中s到t已經沒有通路了,否則還能繼續增廣
我們把s能到的的點集設為S,不能到的點集為T
構造出一個割集C(S,T),S到T的邊必然滿流 否則就能繼續增廣
這些滿流邊的流量和就是當前的流即最大流
把這些滿流邊作為割,就構造出了一個和最大流相等的割
相當於在殘量網路中,源點能到達的結點的各個邊的容量和為最大流
所以如果我們要求一個最小割的邊集,我們只要跑一編最大流,然後在殘量網路中找正向邊殘量為0的邊,那麼這條邊肯定在最小割裡面,
這樣就可以得到一組最小割的邊集
3.最大流等於最小割
設相等的流和割分別為F_m和C_m
則因為任意一個流小於等於任意一個割
任意F≤F_m=C_m≤任意C
費用流
給定一個網路