蓝桥杯-网络流裸题

题目:

一个有向图,求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 )」的「排水方法」。

image-20201010135919914

以图一为例,进水孔为vertex(S),排水孔为vertex(T),中间要经过污水处理站vertex(A)vertex(C)边(edge)代表水管,边的权重(weight)(以下将称为capacity )表示水管的「孔径」。

考虑两种「排水方法」的flow:

  • 第一种分配水流的方法,每单位时间总流量为20

    image-20201010140613238

    • 在Path : S − A − T上每单位时间流了5单位的水;
    • 在Path : S − A − C − T上每单位时间流了10单位的水(问题出在这,占去了edge(C,T)的容量);
    • 在Path : S − C − T上,因为edge(C,T)上只剩下「5单位的容量」,因此每单位时间流了5单位的水。
  • 第二种分配水流的方法,每单位时间总流量为25

    image-20201010140839607

    • 在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)。我们可以利用一个矩阵存储图信息。

image-20201010141311185

  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流过。

image-20201010143003465

以图三为例:

  • 如果在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

    • 其物理意义:可以用重置配置水流方向来理解。

      image-20201010144230200

    • 根据上图表示,我们可以将其看成是:我们已经有了一个通过6个单位的流量的剩余网络,如果现在想经过Path:S - C - A - B - T流过2单位的flow。

    • 根据上图画出的残差图为:

      image-20201010144728020

    • 在图三(a)已经有6单位的流从顶点A流向顶点C,现在可以从edge(A,C)上把2单位的flow"收回",转而分配到edge(A,B)上,而edge(A,C)上就只剩下4单位的流,最后的结果如下图所示:

      image-20201010151731653

      我们根据上图可以看出:流入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)

image-20201010153313334

STEP-2:在Residual Networks上寻找Augmenting Paths

在该Graph中,使用BFS寻找能够从顶点S到顶点T,且edge数最少的路径,PATH = S - A - B - T,见图五(b)。BFS有可能找到其他一条S - C - D - T,这里以前者为例:

image-20201010153720513

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;

image-20201010154328576

重复上述操作,对上述残差图继续寻找增广路径,直到找不到增广路径为止。

代码:

  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;
    	}
    }
    
posted @ 2020-10-10 17:59  SalemG  阅读(272)  评论(0)    收藏  举报