藍橋杯-網路流裸題
題目:
一個有向圖,求1到N的最大流
輸入格式:
第一行N M,表示點數與邊數
接下來M行每行s、t、c。表示一條從s到t的容量為c的邊
輸出格式:
一個數最大流量
樣例輸入:
6 10
1 2 4
1 3 8
2 3 4
2 4 4
2 5 1
3 4 2
3 5 2
4 6 7
5 4 6
5 6 3
樣例輸出
8
資料約定:
n<=1000 m<=10000
預備知識和注意事項:
考慮如下情境:
在某個汙水處理廠的某一道程式裡,有一個「進水孔」,和一個「排水孔」,中間由許多「孔徑不一」的水管連線起來,因為水管的「孔徑大小」會影響到「每單位時間的流量」,因此要解決的問題,就是找到每單位時間可以排放「最大流量( flow )」的「排水方法」。
以圖一為例,進水孔為vertex(S)
,排水孔為vertex(T)
,中間要經過汙水處理站vertex(A)
與vertex(C)
邊(edge)代表水管,邊的權重(weight)(以下將稱為capacity )表示水管的「孔徑」。
考慮兩種「排水方法」的flow:
-
第一種分配水流的方法,每單位時間總流量為
20
:- 在Path :
S − A − T
上每單位時間流了5
單位的水; - 在Path :
S − A − C − T
上每單位時間流了10
單位的水(問題出在這,佔去了edge(C,T)的容量); - 在Path :
S − C − T
上,因為edge(C,T)上只剩下「5
單位的容量」,因此每單位時間流了5
- 在Path :
-
第二種分配水流的方法,每單位時間總流量為
25
:- 在Path :
S − A − T
上每單位時間流了10
單位的水; - 在Path :
S − A − C − T
上每單位時間流了5
單位的水; - 在Path :
S − C− T
上,因為edge(C,T)上剛好還有「10
單位的容量」,因此每單位時間流了10
單位的水;
- 在Path :
從以上兩種「排水方式」可以看得出來,解決問題的精神,就是如何有效利用水管的「孔徑容量」,讓最多的水可以從「進水孔」流到「排水孔」。
這就是在網路流(Flow Networks)上找到最大流量( Maximum Flow )的問題。
網路流的基本性質
Flow Networks
edge(X,Y)
具有非負的capacity,即:c(X,Y)≥0
,如圖二(a)。我們可以利用一個矩陣儲存圖資訊。
- 若不存在
edge(X,Y)
,則定義c(X,Y) = 0
- 特別的,要區分兩個
vertex
:source
:表示Flow Networks
的流量源頭,以s
表示。sink/termination
:表示Flow Networks
的流量終點,以t
表示。
- 水管裡的水流:
flow
,必須滿足以下三個條件a.容量限制 b.反對稱性 c.流守恆性
:- 從頂點X流向頂點Y的流 <=
edge(X,Y)
的capacity
- 以圖二(b)為例,在Path : S − A − C − D − T上的edge之capacity皆大於6,因此在此路徑上流入
6
單位的flow
是可行的。最小的f(X,Y) = 7
,所以流過的flow只要小於等於7
即可。
- 以圖二(b)為例,在Path : S − A − C − D − T上的edge之capacity皆大於6,因此在此路徑上流入
f(X,Y) = -f(Y,X)
,此與電子流(負電荷)與電流(正電荷)的概念雷同- 對有向圖中除了
source
與sink
以外的頂點而言,所有「流進flow」之總和 = 所有「流出flow」的總和
。也就是水流不會無故增加或無故減少,可視為一種守恆。
- 從頂點X流向頂點Y的流 <=
- 可行流:在容量網路中滿足以下條件的網路流flow,成為可行流
- 弧流量限制條件:
0 <= f(u,v) <= c(u,v)
- 平衡條件:即流入一個點的流量要等於流出這個點的流量。(
source
和sink
除外)
- 弧流量限制條件:
最大流量演算法(Ford-Fulkerson演算法)
Ford-Fulkerson演算法需要兩個輔助工具
- Residual Networks(剩餘網路,殘差圖)
- Augmenting Paths(增廣路徑)
Residual Networks(剩餘網路,殘差圖)
Residual Networks的概念為:記錄Graph上之edge還有多少「剩餘的容量」可以讓flow流過。
以圖三為例:
-
如果在Path:
S - A - C - D - T
上所有的edge都有6
單位的flow流過,那麼這些edge(edge(S,A)、edge(A,C)、edge(C,D)、edge(D,T))可用的剩餘capacity
,都應該減6
。例如:edge(S,A)只能在容納9-6=3
單位的flow,edge(C,D)只能容納7-6=1
單位的flow。 -
最關鍵的是,若「從vertex(A)指向vertex(C )」之
edge(A,C)
上,有6
單位的flow流過,即f(A,C)=6
,那麼在其Residual Networks
上,會因應產生出一條「從vertex(C ) 指向vertex(A)」的edge(C,A),並具有6單位的residual capacity,即:cf(C,A) = 6
。 (證明見下) -
證明:這些
residual capacity
稱為:剩餘capacity
以cf
表示。-
cf(C,A) = c(C,A) - f(C,A) = c(C,A) + f(A,C) = 0+6 = 6
-
其物理意義:可以用重置配置水流方向來理解。
-
根據上圖表示,我們可以將其看成是:我們已經有了一個通過
6
個單位的流量的剩餘網路,如果現在想經過Path:S - C - A - B - T
流過2
單位的flow。 -
根據上圖畫出的殘差圖為:
-
在圖三(a)已經有
6
單位的流從頂點A流向頂點C,現在可以從edge(A,C)上把2
單位的flow"收回",轉而分配到edge(A,B)上,而edge(A,C)上就只剩下4
單位的流,最後的結果如下圖所示:我們根據上圖可以看出:流入
sink (或稱termination)
的flow累加到8單位。
-
-
綜上:
- 若edge(X,Y)上有flow流過,即f(X,Y),便將edge(X,Y)上的
Residual Capacity
定義為:cf(X,Y) = c(X,Y) - f(X,Y)
c(X,Y)
表示原來水管孔徑大小;f(X,Y)
表示目前水管已經有多少容量;cf(X,Y)
表示水管還能在容納多少流量;
- 若edge(X,Y)上有flow流過,即f(X,Y),便將edge(X,Y)上的
Augmenting Paths(增廣路徑)
在Residual Networks
裡,所有能夠「從source走到termination」的路徑,也就是所有能夠「增加flow的path」,就稱為Augmenting Paths
。
演演算法
Ford-Fulkerson Algorithm (若使用BFS搜尋路徑,又稱為Edmonds-Karp Algorithm)的方法如下:
- 在
Residual Networks
上尋找Augmenting Paths
- 若以BFS方法尋找,便能確保每次找到的
Augmenting Paths
一定經過最少的edge。(對於所有邊長度相同的情況,比如地圖模型,bfs第一次遇到目標點,此時就一定是從根節點到目標節點最短的路徑【因為每一次所有點都是向外擴張一步,你先遇到就一定是最短】。bfs先找到的一定是最短的)。
- 若以BFS方法尋找,便能確保每次找到的
- 找到
Augmenting Paths
上的最小Residual Capacity
加入總flow,在以最小Residual Capacity
更新Residual Networks
的edge的Residual Capacity
。 - 重複上述步驟,知道再也沒有
Augmenting Paths
為止,便能找到最大流。
例子:
STEP-1:先用flow = 0
對Residual Capacity
進行初始化,如圖五(a)
STEP-2:在Residual Networks
上尋找Augmenting Paths
在該Graph中,使用BFS尋找能夠從頂點S到頂點T,且edge數最少
的路徑,PATH = S - A - B - T
,見圖五(b)。BFS有可能找到其他一條S - C - D - T
,這裡以前者為例:
STEP-3:找到Augmenting Paths
上的最小Residual Capacity
加入總flow
最小Residual Capacity
= 3;
flow = flow + 3;
STEP-4:以最小Residual Capacity
更新Residual Networks
上的edge之residual capacity
cf(S,A) = c(S,A) - f(S,A) = 9 - 3 = 6;
cf(A,S) = c(A,S) - f(A,S) = 0 + 3 = 3;
cf(A,B) = c(A,B) - f(A,B) = 3 - 3 = 0;
cf(B,A) = c(B,A) - f(B,A) = 0 + 3 = 3;
cf(B,T) = c(B,T) - f(B,T) = 9 - 3 = 6;
cf(T,B) = c(T,B) - f(T,B) = 0 + 3 = 3;
重複上述操作,對上述殘差圖繼續尋找增廣路徑,直到找不到增廣路徑為止。
程式碼:
-
建立有向圖Graph,並使用
graph[X][Y]
儲存edge(X,Y)的權重weight
private static void buildGraph(int[][] graph, int vertex1, int vertex2, int weight){ // 因為一條邊可能會出現多次 graph[vertex1][vertex2] += weight; }
-
使用BFS方法進行搜尋,尋找從
source
到sink
的路徑,而且是edge
數量最少的路徑:private static boolean BFSFindPath(int[][] graph, int source, int sink, int[] path) { //path[]是通過記錄每個節點的父節點,從而記錄下一條完整的路徑 //每次尋找都要初始化一次path for(int i = 0; i < path.length; i++) { path[i] = 0; } int vertex_num = graph.length - 1; boolean[] visited = new boolean[vertex_num + 1]; Queue<Integer> queue = new LinkedList<Integer>(); queue.offer(source); visited[source] = true; while(queue.isEmpty() == false) { int temp = queue.poll(); for(int i = 1; i <= vertex_num; i++) { if (graph[temp][i] > 0 && visited[i] == false) { queue.offer(i); visited[i] = true; path[i] = temp; } } } return visited[sink] == true; }
-
找到從BFSFindPath()找到的路徑上,最小的
Residual capacity
。private static int minCapacity(int[] path, int[][] graph) { int min = graph[path[path.length - 1]][path.length - 1]; for (int i = path.length - 2; i != 1; i = path[i]) { if (graph[path[i]][i] < min && graph[path[i]][i] > 0) { //如果不是>0則可能把沒有邊的也算進去。 min = graph[path[i]][i]; } } return min; }
-
演演算法的思路:
int max_flow = 0; int[] path = new int[vertex_num + 1]; // 在Residual Networks上尋找Augmenting Path while(BFSFindPath(graph, 1, vertex_num, path)) { //如果能夠找到Augmenting Path,那麼就在該路徑上尋找最小容量 int min_capacity = minCapacity(path, graph); //更新最大流 max_flow += min_capacity; // 更新殘差圖 for(int i = vertex_num; i != 1; i = path[i]) { int j = path[i]; graph[j][i] -= min_capacity; graph[i][j] += min_capacity; } } System.out.println(max_flow);
-
完整程式碼:
import java.util.LinkedList; import java.util.Queue; import java.util.Scanner; public class Main { public static void main(String[] args) { Scanner sc = new Scanner(System.in); //第一行輸入 節點個數 和 邊的個數 int vertex_num = sc.nextInt(); int edge_num = sc.nextInt(); //初始化二維陣列進行存放資料 int[][] graph = new int[vertex_num + 1][vertex_num + 1]; //每一行輸入進行儲存資料 for(int i = 0; i < edge_num; i++) { int vertex1 = sc.nextInt(); int vertex2 = sc.nextInt(); int weight = sc.nextInt(); // 填充資料,形成一個有向圖 buildGraph(graph, vertex1, vertex2, weight); } // 宣告最大流和Augmenting Path int max_flow = 0; int[] path = new int[vertex_num + 1]; // 在Residual Networks上尋找Augmenting Path while(BFSFindPath(graph, 1, vertex_num, path)) { //如果能夠找到Augmenting Path,那麼就在該路徑上尋找最小容量 int min_capacity = minCapacity(path, graph); //更新最大流 max_flow += min_capacity; // 更新殘差圖 for(int i = vertex_num; i != 1; i = path[i]) { int j = path[i]; graph[j][i] -= min_capacity; graph[i][j] += min_capacity; } } System.out.println(max_flow); } private static int minCapacity(int[] path, int[][] graph) { int min = graph[path[path.length - 1]][path.length - 1]; for (int i = path.length - 2; i != 1; i = path[i]) { if (graph[path[i]][i] < min && graph[path[i]][i] > 0) { //如果不是>0則可能把沒有邊的也算進去。 min = graph[path[i]][i]; } } return min; } /** * 建立有向圖 */ private static void buildGraph(int[][] graph, int vertex1, int vertex2, int weight){ // 因為一條邊可能會出現多次 graph[vertex1][vertex2] += weight; } private static boolean BFSFindPath(int[][] graph, int source, int sink, int[] path) { //path[]是通過記錄每個節點的父節點,從而記錄下一條完整的路徑 //每次尋找都要初始化一次path for(int i = 0; i < path.length; i++) { path[i] = 0; } int vertex_num = graph.length - 1; boolean[] visited = new boolean[vertex_num + 1]; Queue<Integer> queue = new LinkedList<Integer>(); queue.offer(source); visited[source] = true; while(queue.isEmpty() == false) { int temp = queue.poll(); for(int i = 1; i <= vertex_num; i++) { if (graph[temp][i] > 0 && visited[i] == false) { queue.offer(i); visited[i] = true; path[i] = temp; } } } return visited[sink] == true; } }