2018-03-13 19:02:13
在圖論中,網絡流(英語:Network flow)是指在一個每條邊都有容量(capacity)的有向圖分配流,使一條邊的流量不會超過它的容量。通常在運籌學中,有向圖稱為網絡。頂點稱為節點(node)而邊稱為弧(arc)。一道流必須匹配一個結點的進出的流量相同的限制,除非這是一個源點(source)──有較多向外的流,或是一個匯點(sink)──有較多向內的流。一個網絡可以用來模擬道路系統的交通量、管中的液體、電路中的電流或類似一些東西在一個結點的網絡中游動的任何事物。
一、最大流最小割定理
最大流最小割定理提供了對於一個網絡流,從源點到目標點的最大的流量等於最小割的每一條邊的和。
這個定理說明,當網絡達到最大流時,會有一個割集,這個割集中的所有邊都達到飽和狀態。
這等價於在網絡中再也找不到一個從s到t的增廣路徑。
因為只要能找到一條增廣路徑,這條增廣路徑肯定要經過最小割集中的一條邊,否則這個割集就不能稱之為割集了。
既然這個割集中所有的邊都飽和了,因此也就不會存在這樣的增廣路徑了。
這個定理的意義在於給我們指明了方向:
任何算法,只要最后能達到“再也找不到一條增廣路徑”,就可以說明這個算法最后達到了最大流。
二、最大流問題
在優化理論中,最大流問題涉及到在一個單源點、單匯點的網絡流中找到一條最大的流。
最大流問題可以被看作是一個更復雜的網絡流問題(如循環問題(circulation problem))的特殊情況,。s-t流(從源點s到匯點t)的最大值等於s-t割的最小容量,這被稱為最大流最小割定理。
下面舉例來說明這個問題:
問題描述:
給定一個有向圖G=(V,E),把圖中的邊看作管道,每條邊上有一個權值,表示該管道的流量上限。給定源點s和匯點t,現在假設在s處有一個水源,t處有一個蓄水池,問從s到t的最大水流量是多少。
這個問題有如下的一些限制:
- 容量限制:也就是在每條通路上的流量都不能超過其capacity。
- 平衡條件:除了源點和匯點,其他的點的流入量需要等於流出量。
- 反對稱:v到u的凈流量等於u到v的凈流量的相反。

問題求解:
方法一、朴素dfs
一種非常朴素的思路是,使用dfs每次尋找s->t的一條通路,然后將這條路上的流量值定義為其中最小的容量值,完成這次運輸后,將通路上的所有邊的容量值減去流量值,開始下一次的尋找,直到沒有通路,完成算法。
這個算法看上去是沒有什么問題的,因為每次都在增加流量直到不能增加為止,可是真的是這樣么?
我們看下面的這個例子:

如果第一次dfs到的通路是s - > a - > b - > t,那么這次之后,網絡中就再也沒有從s - > t的通路了,按照算法的思路,這個網絡的最大流就是100,然而,很明顯的,這個網絡的最大流是200。
因此,簡單的使用dfs是不能很好的解決這個問題的,下面的Ford-Fulkerson算法解決了這個問題,而成為了網絡流算法中的經典。
方法二、Ford-Fulkerson算法
在上面的例子中出錯的原因是a - > b 的實際流量應該是0,但是我們過早的認為他們之間是有流量的,因此封鎖了我們最大流繼續增大的可能。
一個改進的思路:應能夠修改已建立的流網絡,使得“不合理”的流量被刪掉。
一種實現:對上次dfs 時找到的流量路徑上的邊,添加一條“反向”邊,反向邊上的容量等於上次dfs 時找到的該邊上的流量,然后再利用“反向”的容量和其他邊上剩余的容量尋找路徑。
下面我們重新對上面的例子進行分析:
1、第一次dfs依然是s - > a - > b - > t,與上次不同的是,這次我們會添加上反向邊。

2、繼續在新圖上找可能通路,我們這次可以找到另一條流量為100的通路s - > b - > a - > t,從宏觀上來看,反向邊起到了“取消流”的功能,也可以看成是兩條通路的合並。 
3、對第二次找到的路徑添加反向邊,我們發現圖中已經沒有從s - > t的通路了,於是該網絡的最大流就是200。

添加反向邊的證明:
構建殘余網絡時添加反向邊a->b,容量是n,增廣的時候發現了流量n-k,即新增了n-k的流量。這n-k的流量,從a進,b出,最終流到匯。
現要證明這2n-k的從流量,在原圖上確實是可以從源流到匯的。
在原圖上可以如下分配流量,則能有2n-k從源流到匯點:
三、Ford-Fulkerson算法
Ford-Fulkerson算法:求最大流的過程,就是不斷找到一條源到匯的路徑,然后構建殘余網絡,再在殘余網絡上尋找新的路徑,使 總流量增加,然后形成新的殘余網絡,再尋找新路徑….. 直到某個殘余網絡上找不到從源到匯的路徑為止,最大流就算出來了。
殘余網絡 (Residual Network):在一個網絡流圖上,找到一條源到匯的路徑(即找到了一個流量)后,對路徑上所有的邊,其容量都減去此次找到的流量,對路徑上所有的邊,都添加一條反向邊,其容量也等於此次找到的流量,這樣得到的新圖,就稱為原圖的“殘余網絡”。
增廣路徑: 每次尋找新流量並構造新殘余網絡的過程,就叫做尋找流量的“增廣路徑”,也叫“增廣”。
算法時間復雜度分析:現在假設每條邊 的容量都是整數,這個算法每次都能將流至少增加1。由於整個網絡的流量最多不超過 圖中所有的邊的容量和C,從而算法會結束現在來看復雜度找增廣路徑的算法可以用dfs,復雜度為O(V + E)。dfs最多運行C次所以時間復雜度為C*(V + E) =C * V ^ 2。
四、Edmonds-Karp算法
這個算法實現很簡單但是注意到在圖中C可能很大很大,比如說下面這張圖,如果運氣不好 這種圖會讓你的程序執行200次dfs,雖然實際上最少只要2次我們就能得到最大流。為了避免這個問題,我們每次在尋找增廣路徑的時候都按照bfs進行尋找,而不是按照dfs進行尋找,這就是Edmonds-Karp最短增廣路算法。
時間復雜度:已經證明這種算法的復雜度上限為O(V*E^2)。

import java.util.Arrays;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Scanner;
// POJ 1273
public class EdmondsKarp {
static int[][] G = new int[300][300];
static boolean[] visited = new boolean[300];
static int[] pre = new int[300];
static int v;
static int e;
static int augmentRoute() {
Arrays.fill(visited, false);
Arrays.fill(pre, 0);
Queue<Integer> queue = new LinkedList<Integer>();
queue.add(1);
boolean flag = false;
while (!queue.isEmpty()) {
int cur = queue.poll();
visited[cur] = true;
for (int i = 1; i <= v; i++) {
if (i == cur) continue;
if (G[cur][i] != 0 && !visited[i]) {
pre[i] = cur;
if (i == v) {
flag = true;
queue.clear();
break;
}
else queue.add(i);
}
}
}
if (!flag) return 0;
int minFlow = Integer.MAX_VALUE;
int tmp = v;
while (pre[tmp] != 0) {
minFlow = Math.min(minFlow, G[pre[tmp]][tmp]);
tmp = pre[tmp];
}
tmp = v;
while (pre[tmp] != 0) {
G[pre[tmp]][tmp] -= minFlow;
G[tmp][pre[tmp]] += minFlow;
tmp = pre[tmp];
}
return minFlow;
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
while (sc.hasNext()) {
int res = 0;
for (int i = 0; i < G.length; i++) {
Arrays.fill(G[i], 0);
}
e = sc.nextInt();
v = sc.nextInt();
int s, t, c;
for (int i = 0; i < e; i++) {
s = sc.nextInt();
t = sc.nextInt();
c = sc.nextInt();
G[s][t] += c;
}
int aug;
while ((aug = augmentRoute()) != 0) {
res += aug;
}
System.out.println(res);
}
}
}
五、Dinic算法
Edmonds-Karp已經是一種很不錯的改進方案了,但是每次生成一條增廣路徑都需要對s到t調用BFS,如果能在一次增廣的過程中,尋找到多條增廣路徑,就可以進一步提高算法的運行效率。
Dinic算法就是使用了BFS + DFS達到了以上的思路,完成了算法復雜度的進一步下降。
時間復雜度:O(V^2 * E)
算法流程:
1、使用BFS對殘余網絡進行分層,在分層時,只要進行到匯點的層次數被算出即可停止,因為按照該DFS的規則,和匯點同層或更下一層的節點,是不可能走到匯點的。


2、分完層后,從源點開始,用DFS從前一層向后一層反復尋找增廣路(即要求DFS的每一步都必須要走到下一層的節點)。
3、DFS過程中,要是碰到了匯點,則說明找到了一條增廣路徑。此時要增加總流量的值,消減路徑上各邊的容量,並添加反向邊,即所謂的進行增廣。
4、DFS找到一條增廣路徑后,並不立即結束,而是回溯后繼續DFS尋找下一個增廣路徑。回溯到的結點滿足以下的條件:
1) DFS搜索樹的樹邊(u,v)上的容量已經變成0。即剛剛找到的增廣路徑上所增加的流量,等於(u,v)本次增廣前的容量。(DFS的過程中,是從u走到更下層的v的)
2) u是滿足條件 1)的最上層的節點。
5、如果回溯到源點而且無法繼續往下走了,DFS結束。因此,一次DFS過程中,可以找到多條增廣路徑。
6、DFS結束后,對殘余網絡再次進行分層,然后再進行DFS當殘余網絡的分層操作無法算出匯點的層次(即BFS到達不了匯點)時,算法結束,最大流求出。
一般用棧實現DFS,這樣就能從棧中提取出增廣路徑。
// POJ 1273
public class Dinic { static int[][] G = new int[300][300]; static boolean[] visited = new boolean[300]; static int[] layer = new int[300]; static int v; static int e; static boolean countLayer() { int depth = 0; Arrays.fill(layer, -1); layer[1] = 0; Queue<Integer> queue = new LinkedList<Integer>(); queue.add(1); while (!queue.isEmpty()) { int cur = queue.poll(); for (int i = 1; i <= v; i++) { if (G[cur][i] > 0 && layer[i] == -1) { layer[i] = layer[cur] + 1; if (i == v) { queue.clear(); return true; } queue.add(i); } } } return false; } static int dinic() { int res = 0; List<Integer> stack = new ArrayList<Integer>(); while (countLayer()) { stack.add(1); Arrays.fill(visited, false); visited[1] = true; while (!stack.isEmpty()) { int cur = stack.get(stack.size() - 1); if (cur == v) { int minFlow = Integer.MAX_VALUE; int minS = Integer.MAX_VALUE; for (int i = 1; i < stack.size(); i++) { int tmps = stack.get(i - 1); int tmpe = stack.get(i); if (minFlow > G[tmps][tmpe]) { minFlow = G[tmps][tmpe]; minS = tmps; } } // 生成殘余網絡 for (int i = 1; i < stack.size(); i++) { int tmps = stack.get(i - 1); int tmpe = stack.get(i); G[tmps][tmpe] -= minFlow; G[tmpe][tmps] += minFlow; } // 退棧到minS while (!stack.isEmpty() && stack.get(stack.size() - 1) != minS) { stack.remove(stack.size() - 1); } res += minFlow; } else { int i; for (i = 1; i <= v; i++) { if (G[cur][i] > 0 && layer[i] == layer[cur] + 1 && !visited[i]) { visited[i] = true; stack.add(i); break; } } if (i > v) { stack.remove(stack.size() - 1); } } } } return res; } public static void main(String[] args) { Scanner sc = new Scanner(System.in); while (sc.hasNext()) { for (int i = 0; i < G.length; i++) { Arrays.fill(G[i], 0); } e = sc.nextInt(); v = sc.nextInt(); int s, t, c; for (int i = 0; i < e; i++) { s = sc.nextInt(); t = sc.nextInt(); c = sc.nextInt(); G[s][t] += c; } System.out.println(dinic()); } } }
六、最小費用最大流
問題描述:
設有一個網絡圖 G(V,E) , V={s,a,b,c,…,s ’},E 中的每條邊 (i,j) 對應一個容量 c(i,j) 與輸送單位流量所需費用a(i,j) 。如有一個運輸方案(可行流),流量為 f(i, j) ,則最小費用最大流問題就是這樣一個求極值問題:
![]()
其中 F 為 G 的最大流的集合,即在最大流中尋找一個費用最小的最大流。
算法流程:

反復用spfa算法做源到匯的最短路進行增廣,邊權值為邊上單位費用。反向邊上的單位費用是負的。直到無法增廣,即為找到最小費用最大流。
成立原因:每次增廣時,每增加1個流量,所增加的費用都是最小的。


