1. 程式人生 > 實用技巧 >藍橋杯-網路流裸題

藍橋杯-網路流裸題

題目:

一個有向圖,求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
      單位的水。
  • 第二種分配水流的方法,每單位時間總流量為25

    • 在Path : S − A − T上每單位時間流了10單位的水;
    • 在Path : S − A − C − T上每單位時間流了5單位的水;
    • 在Path : S − C− T上,因為edge(C,T)上剛好還有「10單位的容量」,因此每單位時間流了10單位的水;

從以上兩種「排水方式」可以看得出來,解決問題的精神,就是如何有效利用水管的「孔徑容量」,讓最多的水可以從「進水孔」流到「排水孔」。

這就是在網路流(Flow Networks)上找到最大流量( Maximum Flow )的問題。

網路流的基本性質

Flow Networks

是一個帶權有向圖,其edge(X,Y)具有非負的capacity,即:c(X,Y)≥0,如圖二(a)。我們可以利用一個矩陣儲存圖資訊。

  1. 若不存在edge(X,Y),則定義c(X,Y) = 0
  2. 特別的,要區分兩個vertex
    1. source:表示Flow Networks的流量源頭,以s表示。
    2. sink/termination:表示Flow Networks的流量終點,以t表示。
  3. 水管裡的水流:flow,必須滿足以下三個條件a.容量限制 b.反對稱性 c.流守恆性
    1. 從頂點X流向頂點Y的流 <= edge(X,Y)capacity
      1. 以圖二(b)為例,在Path : S − A − C − D − T上的edge之capacity皆大於6,因此在此路徑上流入6單位的flow是可行的。最小的f(X,Y) = 7,所以流過的flow只要小於等於7即可。
    2. f(X,Y) = -f(Y,X),此與電子流(負電荷)與電流(正電荷)的概念雷同
    3. 對有向圖中除了sourcesink以外的頂點而言,所有「流進flow」之總和 = 所有「流出flow」的總和。也就是水流不會無故增加或無故減少,可視為一種守恆。
  4. 可行流:在容量網路中滿足以下條件的網路流flow,成為可行流
    1. 弧流量限制條件:0 <= f(u,v) <= c(u,v)
    2. 平衡條件:即流入一個點的流量要等於流出這個點的流量。(sourcesink除外)

最大流量演算法(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稱為:剩餘capacitycf表示。

    • 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)表示水管還能在容納多少流量;

Augmenting Paths(增廣路徑)

Residual Networks裡,所有能夠「從source走到termination」的路徑,也就是所有能夠「增加flow的path」,就稱為Augmenting Paths

演演算法

Ford-Fulkerson Algorithm (若使用BFS搜尋路徑,又稱為Edmonds-Karp Algorithm)的方法如下:

  1. Residual Networks上尋找Augmenting Paths
    1. 若以BFS方法尋找,便能確保每次找到的Augmenting Paths一定經過最少的edge。(對於所有邊長度相同的情況,比如地圖模型,bfs第一次遇到目標點,此時就一定是從根節點到目標節點最短的路徑【因為每一次所有點都是向外擴張一步,你先遇到就一定是最短】。bfs先找到的一定是最短的)。
  2. 找到Augmenting Paths上的最小Residual Capacity加入總flow,在以最小Residual Capacity更新Residual Networks的edge的Residual Capacity
  3. 重複上述步驟,知道再也沒有Augmenting Paths為止,便能找到最大流。

例子:

STEP-1:先用flow = 0Residual 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;

重複上述操作,對上述殘差圖繼續尋找增廣路徑,直到找不到增廣路徑為止。

程式碼:

  1. 建立有向圖Graph,並使用graph[X][Y]儲存edge(X,Y)的權重weight

    private static void buildGraph(int[][] graph, int vertex1, int vertex2, int weight){
        // 因為一條邊可能會出現多次
    	graph[vertex1][vertex2] += weight;
    	}
    
  2. 使用BFS方法進行搜尋,尋找從sourcesink的路徑,而且是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;
    }
    
  3. 找到從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;
    }
    
  4. 演演算法的思路:

    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);
    
  5. 完整程式碼:

    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;
    	}
    }