[洛谷P3376題解]網路流(最大流)的實現演算法講解與程式碼

更壞的閱讀體驗

定義

對於給定的一個網路,有向圖中每個的邊權表示可以通過的最大流量。假設出發點S水流無限大,求水流到終點T後的最大流量。

起點我們一般稱為源點,終點一般稱為匯點

內容前置

1.增廣路

​ 在一個網路源點S匯點T的一條各邊剩餘流量都大於0(還能讓水流通過,沒有堵住)的一條路。

2.分層

​ 預處理出源點到每個點的距離(每次尋找增廣路都要,因為以前原本能走的路可能因為水灌滿了,導致不能走了).作用是保證只往更遠的地方放水,避免兜圈子或者是沒事就走回頭路(正所謂人往高處走水往低處流).

3.當前弧優化

​ 每次增廣一條路後可以看做“榨乾”了這條路,既然榨乾了就沒有再增廣的可能了。但如果每次都掃描這些“枯萎的”邊是很浪費時間的。那我們就記錄一下“榨取”到那條邊了,然後下一次直接從這條邊開始增廣,就可以節省大量的時間。這就是當前弧優化

具體怎麼實現呢,先把鏈式前向星的head陣列複製一份,存進cur陣列,然後在cur陣列中每次記錄“榨取”到哪條邊了。

[#3 引用自](Dinic當前弧優化 模板及教程 - Floatiy - 部落格園 (cnblogs.com))

解決演算法

Ford-Fulkerson 演算法(以下簡稱FF演算法)

FF演算法的核心是找增廣路,直到找不到為止。(就是一個搜尋,用盡可能多的水流填充每一個點,直到沒有水用來填充,或者沒有多餘的節點讓水流出去)。

但是這樣的方法有點基於貪心的演算法,找到反例是顯而易見的,不一定可以得到正解。

為了解決這種問題,我們需要一個可以吃後悔藥的方法——加反向邊

原本我們的DFS是一條路走到黑的,現在我們每次進入一個節點,把水流送進去,同時建立一個權值與我們送入的水流量相等,但是方向相反的路(挖一條路讓水流能夠反向流回來,相當於給水流吃一顆後悔藥)。

我們給了FF演算法一顆後悔藥之後就可以讓他能夠找到正確的最大流。

Ford-Fulkerson演算法的複雜度為\(O(e \times f)\) ,其中 \(e\) 為邊數, \(f\)為最大流

上程式碼。

#include <iostream>
#include <cstring>
using namespace std; #define INF 0x3f3f3f3f3f3f3f3f typedef long long ll; // Base
const int N= 256;
const int M= 8192*2;
// End // Graph
int head[N],nxt[M],to[M];
ll dis[M];
int p; inline void add_edge(int f,int t,ll d)
{
to[p]=t;
dis[p]=d;
nxt[p]=head[f];
head[f]=p++;
}
// End int n,m,s,t; // Ford-Fulkerson bool vis[N]; ll dfs(int u,ll flow)//u是當前節點 , flow是送過來的水量
{
if(u==t)// End,水送到終點了
return flow;
vis[u]=true; for(int i=head[u];i!=-1;i=nxt[i])
{
ll c;//存 送水下一個節點能通到終點的最大流量
if(dis[i]>0 //如果水流還能繼續流下去
&& !vis[to[i]] //並且要去的點沒有其他的水流去過
&& (c=dfs(to[i],min(flow,dis[i])))!=-1//根據木桶效應,能傳給下一個節點的水量取決於當前節點有的水量與管道(路徑)能夠輸送的水量的最小值
//要保證這條路是通的我們才可以向他送水,不然就是浪費了
) {
dis[i]-=c;//這個管道已經被佔用一部分用來送水了,需要減掉
dis[i^1]+=c;//給他的反向邊加上相同的水量,送後悔藥
//至於為什麼是這樣取出反向邊,下面有講
return c;
}
}
return -1;
}
// End
int main()
{
ios::sync_with_stdio(true); memset(head,-1,sizeof(head));// init cin>>n>>m>>s>>t;
for(int i=1;i<=m;i++)
{
int u,v,w;cin>>u>>v>>w;
add_edge(u,v,w);
add_edge(v,u,0);//建立一條暫時無法通水的反向邊(後面正向邊送水後,需要加上相同的水量)
//第一條邊 編號是 0 ,其反向邊為 1, 眾所周知的 奇數^1=奇數-1, 偶數^1=偶數+1 ,利用這種性質 ,我們就可以很快求出反向邊,或者反向邊得出正向邊(這裡說的正反只是相對)
} //Ford-Fulkerson
ll ans = 0;
ll c;
// 假設我們的水無限多
while((c=dfs(s,INF)) != -1) //把源點還能送出去的水全部都送出去,直到送不到終點
{
memset(vis,0,sizeof(vis)); //重新開始送沒送出去的水
ans+=c;//記錄總的水量
}
cout<<ans<<endl;
return 0;
}

可以看出效率比較低,我這裡開了O2也過不了模板題。

Edmond-Karp 演算法(以下簡稱EK演算法)

上面FF演算法太慢了,原因是因為FF演算法太死腦筋了,非要等現在節點水灌滿了,才會灌其他的(明明有一個更大的水管不灌)。另外他有時候還非常謙讓,等到明明走到了,卻要返回去等別人水灌好,再灌自己的。

其實,EK演算法便是FF演算法的BFS版本。複雜度為\(O(v \times e^2)\)​(複雜度這麼高行得通嗎,當然可以,事實上一般情況下根本達不到這麼高的上限)。

那我就直接上程式碼了。

#include <iostream>
#include <cstring>
#include <queue>
using namespace std; #define INF 0x3f3f3f3f3f3f3f3f typedef long long ll; // Base
const int N= 256;
const int M= 8192*2;
// End // Graph
int head[N],nxt[M],to[M];
ll dis[M];
int p; inline void add_edge(int f,int t,ll d)
{
to[p]=t;
dis[p]=d;
nxt[p]=head[f];
head[f]=p++;
}
// End int n,m,s,t; // Edmond-Karp
int last[N];
ll flow[N];//記錄當前的點是哪條邊通到來的,這樣多餘的水又可以這樣送回去. inline bool bfs() //水還能送到終點返回true,反之false
{
memset(last,-1,sizeof last);
queue <int > Q;
Q.push(s);
flow[s] = INF; //把起點的水量裝填到無限大
while(!Q.empty())
{
int k=Q.front();
Q.pop();
if(k==t)// End,水送到終點了
break;
for(int i=head[k];i!=-1;i=nxt[i])
{
if(dis[i]>0 //如果水流還能繼續流下去
&& last[to[i]]==-1 //並且要去的點沒有其他的水流去過,所以last[to[i]]還是-1
){
last[to[i]]=i; // 到 to[i]點 需要走 i這條邊
flow[to[i]]=min(flow[k],dis[i]);//根據木桶效應,能傳給下一個節點的水量取決於當前節點有的水量與管道(路徑)能夠輸送的水量的最小值
Q.push(to[i]); //入隊
}
}
}
return last[t]!=-1;//能夠送到終點
}
// End int main()
{
ios::sync_with_stdio(true);
memset(head,-1,sizeof(head));// init cin>>n>>m>>s>>t;
for(int i=1;i<=m;i++)
{
int u,v,w;cin>>u>>v>>w;
add_edge(u,v,w);
add_edge(v,u,0);//建立一條暫時無法通水的反向邊(後面正向邊送水後,需要加上相同的水量)
//第一條邊 編號是 0 ,其反向邊為 1, 眾所周知的 奇數^1=奇數-1, 偶數^1=偶數+1 ,利用這種性質 ,我們就可以很快求出反向邊,或者反向邊得出正向邊(這裡說的正反只是相對)
}
// Edmond-Karp
ll maxflow=0;
while(bfs())//把源點還能送出去的水全部都送出去,直到送不到終點
{
maxflow+=flow[t];
for(int i=t;i!=s;i=to[last[i]^1])//還有多餘的水殘留在管道里,怪可惜的,原路送回去.
{
dis[last[i]]-=flow[t]; //這個管道已經被佔用一部分用來送水了,需要減掉
dis[last[i]^1]+=flow[t]; //給他的反向邊加上相同的水量,送後悔藥
//至於為什麼是這樣取出反向邊,上面有講
}
}
cout<<maxflow<<endl;
//
return 0;
}

於是我們AC了這題。

還能不能更快? Dinic演算法

FFEK演算法都有個比較嚴重的問題.他們每次都只能找到一條增廣路(到終點沒有被堵上的路).Dinic演算法不僅用到了DFS,還用的了BFS.但是他們發揮的作用是不一樣的。

種類 作用
DFS 尋找路
BFS 分層(內容前置裡有講哦)

Dinic快就快在可以多路增廣(兵分三路把你幹掉),這樣我們可以節省很多走重複路徑的時間.當找到一條增廣路後,DFS會嘗試用剩餘的流量向其他地方擴充套件.找到新的增廣路。

就這???

當然不止,Dinic還有當前弧優化(前面也有哦),總之就是放棄被榨乾的路。

這樣的一通操作之後,複雜度來到了\(O(v^2 \times e)\)​

#include <iostream>
#include <cstring>
#include <queue>
using namespace std; #define INF 0x3f3f3f3f3f3f3f3f typedef long long ll; // Base
const int N = 256;
const int M = 8192 * 2;
// End // Graph
int head[N], nxt[M], to[M];
ll dis[M];
int p; inline void add_edge(int f, int t, ll d)
{
to[p] = t;
dis[p] = d;
nxt[p] = head[f];
head[f] = p++;
}
// End int n, m, s, t; //Dinic
int level[N], cur[N];
//level是各點到起點的深度,cur為當前弧優化的增廣起點 inline bool bfs() //分層函式,其實就是個普通廣度優先搜尋,沒什麼好說的,作用是計算邊權為1的圖,圖上各點到源點的距離
{
memset(level, -1, sizeof(level));
level[s] = 0;
memcpy(cur, head, sizeof(head));
cur[s]=head[s];
queue<int> Q;
Q.push(s);
while (!Q.empty())
{
int k = Q.front();
Q.pop(); for (int i = head[k]; i != -1; i = nxt[i])
{
//還能夠通水的管道才有價值
if (dis[i] > 0 && level[to[i]] == -1)
{
level[to[i]] = level[k] + 1;
Q.push(to[i]);
if(to[i]==t) return true;
}
}
}
return false;
} ll dfs(int u, ll flow)
{
if (u == t)
return flow; ll flow_now = flow; // 剩餘的流量
for (int i = cur[u]; i != -1 && flow_now > 0; i = nxt[i])
{
cur[u] = i; //當前弧優化 //如果水流還能繼續流下去 並且 是向更深處走的
if (dis[i] > 0 && level[to[i]] == level[u] + 1)
{
ll c = dfs(to[i], min(dis[i], flow_now));
if(!c) level[to[i]]=-1; //剪枝,去除增廣完畢的點 flow_now -= c; //剩餘的水流被用了c dis[i] -= c; //這個管道已經被佔用一部分用來送水了,需要減掉
dis[i ^ 1] += c; //給他的反向邊加上相同的水量,送後悔藥
//至於為什麼是這樣取出反向邊,下面有講
}
}
return flow - flow_now; //返回用掉的水流
} //End int main()
{
ios::sync_with_stdio(true);
memset(head, -1, sizeof(head)); // init cin >> n >> m >> s >> t;
for (int i = 1; i <= m; i++)
{
int u, v, w;
cin >> u >> v >> w;
add_edge(u, v, w);
add_edge(v, u, 0); //建立一條暫時無法通水的反向邊(後面正向邊送水後,需要加上相同的水量)
//第一條邊 編號是 0 ,其反向邊為 1, 眾所周知的 奇數^1=奇數-1, 偶數^1=偶數+1 ,利用這種性質 ,我們就可以很快求出反向邊,或者反向邊得出正向邊(這裡說的正反只是相對)
} //Dinic
ll ans = 0;
while (bfs())
ans += dfs(s, INF);
cout << ans << endl;
return 0;
}

這個演算法如果應用在二分圖裡,複雜度為\(O(v \times sqrt(e))\)

參考文獻

1.《演算法競賽進階指南》作者:李煜東

2.《[演算法學習筆記(28): 網路流](演算法學習筆記(28): 網路流 - 知乎 (zhihu.com))》 作者:Pecco

3.《[Dinic當前弧優化 模板及教程](Dinic當前弧優化 模板及教程 - Floatiy - 部落格園 (cnblogs.com))》作者:Floatiy