最大流算法
目錄
- 最大流算法
- 網(wǎng)絡(luò)流基礎(chǔ)概念
- 網(wǎng)絡(luò)流
- 可行流
- 最大流
- 求解最大流算法
- 反向邊
- 增廣路經(jīng)
- EK算法
- Dinic算法
最大流算法
網(wǎng)絡(luò)流基礎(chǔ)概念
網(wǎng)絡(luò)流
在一個有向圖G=(V,E)G= (V,E)G=(V,E)中:
- 有一個唯一的源點S(入度為000:出發(fā)點)
- 有一個唯一的匯點T(出度為000:結(jié)束點)
- 圖中的每一條邊都一個非負(fù)的權(quán)值,這個權(quán)值叫做容量c(u,v)c(u, v)c(u,v)
滿足上述條件的圖GGG稱為網(wǎng)絡(luò)流圖,記為G=(V,E,C)G= (V,E,C)G=(V,E,C)
可以想象成,如果把每條邊都看成一個管道,可以看成是水從源點S出發(fā)經(jīng)過這些管道,最終流向匯點T,而每條管道有最大的容量。
例如:
可行流
流量:每條弧上給定一個實數(shù)f(u,v)f(u,v)f(u,v),滿足0≤f(u,v)≤c(u,v)0 \leq f(u,v) \leq c(u,v)0≤f(u,v)≤c(u,v)
可行流滿足:
- 源點S:流出量 = 整個網(wǎng)絡(luò)的流量
- 匯點T:流入量 = 整個網(wǎng)絡(luò)的流量
- 中間的點:總流入量 = 總流出量,同時0≤f(u,v)≤c(u,v)0 \leq f(u,v) \leq c(u,v)0≤f(u,v)≤c(u,v)
這樣的在整個網(wǎng)絡(luò)中的流量就是可行流,可行流有多個
例如:
紅色的部分表示流量,黑色的表示容量。可行流就是7
最大流
- 所有可行流中流量最大的流量
- 最大流可能不止一個
最大流為11
求解最大流算法
反向邊
- 首先要明白一點,在一條從SSS到TTT的路徑中,能夠帶來的最大流量取決于這條路徑上的最小容量。
對于下面這張圖111是源點,444是匯點
如果我們使用搜索算法找一條從1到4的路徑,并且這條路徑能夠帶來的流量是這條路徑上邊的容量的最小值,
假設(shè)我們找到的路徑是1?>2?>3?>41->2->3->41?>2?>3?>4,現(xiàn)在的流量是111,因為這條路已經(jīng)使用過了,把這條路徑上的每條邊減111,再次找從1到4的路徑,發(fā)現(xiàn)找不到了,因此得到的答案為1,但是正確的答案應(yīng)該是222。即1?>2?>41->2->41?>2?>4、1?>3?>41->3->41?>3?>4
這個時候,為了能夠繼續(xù)找到路徑,必須要反向建立邊,把當(dāng)前路徑反向建邊,邊的權(quán)值就是這條路徑的流量
如圖
其中紅色的邊就是反向建立的,這個時候繼續(xù)尋找一條路徑,發(fā)現(xiàn)可以走1?>3?>2?>41->3->2->41?>3?>2?>4,帶來的流量是111,然后繼續(xù)找尋路徑,發(fā)現(xiàn)沒有可達的路徑了,因此最大流是1+1=21+1=21+1=2。
為什么要反向建邊呢,仔細想想應(yīng)該能夠明白,反向建立邊的作用相當(dāng)于讓之前的路徑有可以反悔的余地。這樣即使一開始走錯也沒有關(guān)系,因為可以通過反向邊來反悔,最終一定能得到正確答案
增廣路經(jīng)
明白了反向建邊后,來看什么是增廣路經(jīng)?
如果一個可行流不是最大流,那么當(dāng)前網(wǎng)絡(luò)中一定存在一條增廣路經(jīng)。
從源點SSS到匯點TTT的一條路徑中,如果邊(u,v)(u,v)(u,v)與該路徑的方向一致就稱為正向邊,否則就記為逆向邊,如果在這條路徑上的所有邊滿足
- 正向邊f(u,v)<c(u,v)f(u,v) <c(u,v)f(u,v)<c(u,v)
- 逆向邊f(u,v)>0f(u, v) > 0f(u,v)>0
則該路徑是增廣路徑
其實增廣路徑就是通過這樣一條路徑,來增加到達匯點的流量,而路徑中的流量沒到達容量。
(在上圖中,其實每次找到的一條從111到444的路徑都是一條增廣路徑)
沿這條增廣路改進可行流的操作稱為增廣.所有可能的增廣路徑放在一起就構(gòu)成了殘余網(wǎng)絡(luò)
以下這222個算法,都是基于不斷找增廣路經(jīng)來實現(xiàn)的
EK算法
復(fù)雜度:O(nm2)O(nm^2)O(nm2),nnn是點數(shù),mmm是邊數(shù)
首先要考慮的是怎么找增廣路徑,之前說用搜索算法,可以用bfsbfsbfs也可以用dfsdfsdfs,但是bfsbfsbfs的好處在于能夠在殘余網(wǎng)絡(luò)中每一次找到最短的一條增廣路徑。因此在EKEKEK算法是基于bfsbfsbfs來找增廣路經(jīng),bfsbfsbfs每執(zhí)行一次,就找出一條增廣路徑來,然后把這條路徑上的權(quán)值進行修改,同時反向建邊。直到找不到增廣路徑為止,算法就結(jié)束了(代碼注釋寫的很詳細)
步驟
- 利用bfsbfsbfs找到一條最短增廣路徑,記錄該路徑的最小流量
- 利用一個數(shù)組把這個路徑上的流量更新
- 不斷重復(fù)1,21,21,2直到?jīng)]有增廣路徑為止
具體實現(xiàn):
POJ 1273(模板題)題目鏈接
代碼
#include <iostream> #include <cstring> #include <cstdio> #include <queue> #define N 205 #define INF 0x7fffffff #define ll long long ll min(ll a, ll b) {return a > b ? b : a; } using namespace std; // Ek 找最大流,其中源點是1,匯點是n ll g[N][N], pre[N], dis[N]; //g用來存圖,pre[i]表示當(dāng)前節(jié)點i的前一個節(jié)點,dis[i]表示從源點到i點的路徑上的最小流量 ll n, m, s, t, ans; queue <ll> q; ll bfs () { //找到一條增廣路經(jīng), 返回這條路徑的流量for(int i = 1; i <= n; i++) pre[i] = -1;while(!q.empty()) q.pop();q.push(s);dis[s] = INF;while (!q.empty()) {ll x = q.front();q.pop();if(x == t) break; //找到了匯點for(int i = 1; i <= n; i++) {if(pre[i] == -1 && g[x][i]) { //找到一個沒有被訪問且還有容量的點pre[i] = x;dis[i] = min(dis[x], g[x][i]); //更新增廣路徑上的最小流量q.push(i);}}}if(pre[t] == -1) return -1; //說明沒有增廣路徑了,因此才走不到匯點else return dis[t]; }void EK () { //更新殘余網(wǎng)絡(luò)的流量ll inc;while (1) {inc = bfs();if(inc == -1) break; //沒有增廣路徑了,算法結(jié)束ll k = t;while (k != s) {g[k][pre[k]] += inc; //建立反向弧g[pre[k]][k] -= inc; //正向容量減去流量k = pre[k];}ans += inc;} }int main () {ll u, v, cost;while(scanf("%lld %lld", &m, &n) != EOF) { //讀入數(shù)據(jù),記得初始化memset(dis, 0, sizeof(dis));memset(g, 0, sizeof(g));ans = 0;s = 1, t = n;for(int i = 1; i <= m; i++) {scanf("%lld %lld %lld", &u, &v, &cost);g[u][v] += cost;}EK();printf("%lld\n", ans);}return 0; }Dinic算法
復(fù)雜度:O(V2E)O(V^2E)O(V2E)
EKEKEK算法找增廣路徑是基于bfsbfsbfs來進行的,bfsbfsbfs會把周圍能夠擴展的點全部擴展進來,直到找到匯點為止,相當(dāng)于每找一次增廣路徑都要搜索大量的點。DinicDinicDinic算法實際上是對EKEKEK的優(yōu)化
DinicDinicDinic算法利用bfsbfsbfs建立分層網(wǎng)絡(luò) (所謂分層網(wǎng)絡(luò),就是按照每一個點到源點的距離,分層,方便后面的dfsdfsdfs)。然后基于這個分層網(wǎng)絡(luò),使用dfsdfsdfs找到當(dāng)前分層網(wǎng)絡(luò)下的所有增廣路徑,并且做好相應(yīng)的修改。然后不斷重復(fù)這兩個過程,直到無法分層為止,這樣做只需要一次bfsbfsbfs就可以找到多條增廣路徑。
(PS:分層網(wǎng)絡(luò),就是利用bfsbfsbfs特性,源點為起點,一直擴散出去,每一個點都打一個標(biāo)記,標(biāo)記到源點的路徑所經(jīng)過的最少的弧的數(shù)量,假設(shè)用dis[i]dis[i]dis[i]表示iii到源點S的最少經(jīng)過的弧的數(shù)量,這樣就可以將整個網(wǎng)絡(luò)分層,基于這個分層網(wǎng)絡(luò),dfsdfsdfs找增廣路徑的時候,就可以找到最短的增廣路徑。如果當(dāng)前點是xxx,dfsdfsdfs搜到下一個點iii,一定要滿足dis[i]=dis[x]+1dis[i] = dis[x] +1dis[i]=dis[x]+1,這樣才是最短增廣路徑,效率才是最高的。)
步驟
- 利用bfsbfsbfs建立分層網(wǎng)絡(luò)(記錄每個節(jié)點的深度)
- 按照當(dāng)前分層網(wǎng)絡(luò)進行dfsdfsdfs(一層一層找),找到所有該分層網(wǎng)絡(luò)下的增廣路徑,并更新殘余網(wǎng)絡(luò)
- 重復(fù)1、21、21、2直到無法建立分層網(wǎng)絡(luò)為止
鄰接矩陣實現(xiàn)
POJ 1273(模板題)題目鏈接
#include <iostream> #include <cstring> #include <cstdio> #include <queue> #define N 205 #define INF 0x7fffffff #define min(a,b) a>b?b:a using namespace std;int n, m, s, t, maxflow; int deep[N], g[N][N]; // deep 表示從源點到當(dāng)前節(jié)點的深度 queue <int> q;bool bfs () { //建立分層網(wǎng)絡(luò)while(!q.empty()) q.pop();memset(deep, -1, sizeof(deep));deep[s] = 0; // 源點的深度為0q.push(s);while(!q.empty()) {int x = q.front();q.pop();for(int i = 1; i <= n; i++) {if(g[x][i] > 0 && deep[i] == -1) {// 如果有容量能夠到達i,并且i節(jié)點的深度未被標(biāo)記deep[i] = deep[x] + 1;q.push(i);}}}if(deep[n] > 0) return true; //能夠建立分層圖else return false; //不存在分層網(wǎng)絡(luò)時,算法結(jié)束 }//基于當(dāng)前分層圖,找到所有增廣路徑 int dfs (int x, int mx) { //表示當(dāng)前節(jié)點x,以及這條增廣路經(jīng)上的最小流量int a;if(x == t) return mx;for(int i = 1; i <= n; i++) {if( g[x][i] > 0 && deep[i] == deep[x] + 1 && (a = dfs(i, min(mx, g[x][i]))) ) {//找到x能流通過去的的相鄰頂點ig[x][i] -= a; //更新殘余網(wǎng)絡(luò)g[i][x] += a;return a;}}return 0; }void dinic () {while(bfs())maxflow += dfs(s, INF); }int main () {int u, v, cost;while(scanf("%d %d", &m, &n) != EOF) {s = 1, t = n;memset(g, 0, sizeof(g));maxflow = 0;for(int i = 1; i <= m; i++) {scanf("%d %d %d", &u, &v, &cost);g[u][v] += cost;}dinic();printf("%d\n", maxflow);}return 0; }鏈?zhǔn)角跋蛐菍崿F(xiàn)(內(nèi)存小)
其中需要注意的細節(jié):
-
存邊的時候反邊的容量設(shè)置為0;
-
假設(shè)dfsdfsdfs在遞歸回來的時候找到了這條增廣路徑的流量為flowflowflow,需要更新正反邊
- 正邊:edge[i].cap    ?=    flowedge[i].cap \,\,\,\,-= \,\,\,\,flowedge[i].cap?=flow
- 反邊:edge[i^1].cap    +=    flowedge[i \hat{} 1].cap\,\,\,\, +=\,\,\,\, flowedge[i^1].cap+=flow
- 其中^\hat{}^表示按位異或,如果iii是偶數(shù)則i^1=i+1i \hat{} 1 = i+1i^1=i+1,如果iii是奇數(shù)則i^1=i?1i \hat{} 1 = i -1i^1=i?1,為什么反邊就是i^1i \hat{} 1i^1呢。因此在存邊的時候,是先存的正邊,然后++cnt++cnt++cnt后存反邊,正反邊差111個,為了保證正邊的編號是偶數(shù),反邊是奇數(shù),cntcntcnt初始化為?1-1?1(因為我的習(xí)慣是寫++cnt++cnt++cnt)
洛谷 網(wǎng)絡(luò)最大流(題目鏈接)
#include <cstdio> #include <iostream> #include <cstring> #include <queue> #define INF 0x7fffffff #define N 10001 #define M 100005 using namespace std;struct flow {int to, next, cap; }edge[M * 4];int n, m, s, t, maxflow; int cnt = -1; int head[N], dep[N]; //其中dep[i]表示i的深度,也就是到源點的最小邊數(shù) queue <int> q; inline void addEdge (int u, int v, int cost) {edge[++cnt].to = v;edge[cnt].cap = cost;edge[cnt].next = head[u];head[u] = cnt;//反向建邊,容量為0edge[++cnt].to = u;edge[cnt].cap = 0;edge[cnt].next = head[v];head[v] = cnt; }bool bfs () { //建立分層網(wǎng)絡(luò)while(!q.empty()) q.pop();memset(dep, -1, sizeof(dep));dep[s] = 0;q.push(s);while(!q.empty()) {int x = q.front(); q.pop();for(int i = head[x]; i != -1; i = edge[i].next) {int u = edge[i].to;int cap = edge[i].cap;if(cap > 0 && dep[u] == -1) {dep[u] = dep[x] + 1;q.push(u);}}}if(dep[t] == -1) return 0;else return 1; }int dfs (int x, int mx) { int a;if(x == t) return mx; //找到匯點,返回for(int i = head[x]; i != -1; i = edge[i].next) {int u = edge[i].to;int cap = edge[i].cap;if(cap > 0 && dep[u] == dep[x] + 1 && (a = dfs(u, min(cap, mx))) ) {edge[i].cap -= a; edge[i^1].cap += a; //反向邊return a;}}return 0; //搜不到增廣路經(jīng)了就返回0 } void dinic () {int res;while ( bfs() ) //當(dāng)前網(wǎng)絡(luò)下,搜索所有的增廣路徑while (1) {res = dfs(s, INF); //加上該路徑能帶來的流量if(!res) break;maxflow += res;} }int main () {int u, v, cost;memset(head, -1, sizeof(head));scanf("%d %d %d %d", &n, &m, &s, &t);for(int i = 1; i <= m; i++) {scanf("%d %d %d", &u, &v, &cost);addEdge(u, v, cost);}dinic();printf("%d", maxflow);return 0; }算法還可以優(yōu)化,加入一個curcurcur數(shù)組。
首先要明白在dfsdfsdfs找增廣路徑的時候,一定是完全增廣的,也就是說這條路徑使用過后,下一次不必再次檢查這條路徑了。直接從下一條邊開使找,加入這個優(yōu)化,算法能快不少
int cur[N]; bool bfs () {while(!q.empty()) q.pop();for(int i = 0; i <= n; i++) cur[i] = head[i]; //復(fù)制head數(shù)組memset(dep, -1, sizeof(dep));dep[s] = 0;q.push(s);while(!q.empty()) {int x = q.front(); q.pop();for(int i = head[x]; i != -1; i = edge[i].next) {int u = edge[i].to;int cap = edge[i].cap;if(cap > 0 && dep[u] == -1) {dep[u] = dep[x] + 1;q.push(u);}}}if(dep[t] == -1) return 0;else return 1; } int dfs (int x, int mx) {int a;if(x == t) return mx;for(int i = cur[x]; i != -1; i = edge[i].next) {cur[x] = i; //避免了搜尋已經(jīng)增廣過的路徑int u = edge[i].to;int cap = edge[i].cap;if(cap > 0 && dep[u] == dep[x] + 1 && (a = dfs(u, min(cap, mx))) ) {edge[i].cap -= a;edge[i^1].cap += a; //反向邊return a;}}return 0; //搜不到增廣路經(jīng)了就返回0 }總結(jié)
- 上一篇: linux脚本写的计算器,Linux b
- 下一篇: 关于异常:HttpURLConnecti