一、什么是最大流问题
假设现在有一个地下水管道网络,有m根管道,n个管道交叉点,现在自来水厂位于其中一个点,向网络中输水,隔壁老王在另外一个点接水,已知由于管道修建的年代不同,有的管道能承受的水流量较大,有的较小,现在求在自来水厂输入的水不限的情况下,隔壁老王能接到的水的最大值?
为解决该问题,可以将输水网络抽象成一个联通的有向图,每根管道是一条边,交叉点为一个结点,从u流向v的管道能承受的最大流量称为容量,设为cap[u][v],而该管道实际流过的流量设为flow[u][v],自来水厂称为源点s,隔壁老王家称为汇点t,则该问题求的是最终流入汇点的总流量flow的最大值。
二、思路分析
关于最大流问题的解法大致分为两类:增广路算法和预流推进算法。增广路算法的特点是代码量小,适用范围广,因此广受欢迎;而预流推进算法代码量比较大,经常达到200+行,但运行效率略高,如果腹黑的出题人要卡掉大多数人的code,那么预流推进则成为唯一的选择。。。。( ⊙ o ⊙ )
咳咳。。。先来看下增广路算法:
为了便于理解,先引入一个引理:最大流最小割定理。
在一个连通图中,如果删掉若干条边,使图不联通,则称这些边为此图的一个割集。在这些割集中流量和最小的一个称为最小割。
最大流最小割定理:一个图的最大流等于最小割。
大开脑洞一下,发现此结论显而易见,故略去证明(其实严格的证明反而不太好写,但是很容易看出结论是对的,是吧)。这便是增广路算法的理论基础。
在图上从s到t引一条路径,给路径输入流flow,如果此flow使得该路径上某条边容量饱和,则称此路径为一条增广路。增广路算法的基本思路是在图中不断找增广路并累加在flow中,直到找不到增广路为止,此时的flow即是最大流。可以看出,此算法其实就是在构造最小割。
增广路算法
而预流推进算法的思路比较奇葩(没找到比较好的图,只能自行脑补一下了。。= =#):
先将s相连的边流至饱和,这种边饱和的结点称为活动点, 将这些活动点加入队列,每次从中取出一个点u,如果存在一个相邻点v是非活动点,则顺着边u->v 推流,直到u变为非活动点。重复此过程,直到队列空,则此时图中的流flow即是最大流。
三、SAP算法
最短增广路算法(shortest arguement-path algorithm),简称SAP。目前应用最广的算法,代码简短又很好理解,一般情况下效率也比较高。属于增广路算法的一种,特别之处是每次用bfs找的是最短的路径,复杂度为O(n*m^2)。
代码如下:
1 #include<stdio.h> 2 3 #include<string.h> 4 5 #include<queue> 6 7 #include<iostream> 8 9 using namespace std; 10 11 12 13 const int maxn = 300; 14 15 const int INF = 1000000+10; 16 17 18 19 int cap[maxn][maxn]; //流量 20 21 int flow[maxn][maxn]; //容量 22 23 int a[maxn]; //a[i]:从起点 s 到 i 的最小容量 24 25 int p[maxn]; //p[i]: 记录点 i 的父亲 26 27 28 29 int main() 30 31 { 32 33 int n,m; 34 35 while(~scanf("%d%d", &n,&m)) 36 37 { 38 39 memset(cap, 0, sizeof(cap)); //初始化容量为 0 40 41 memset(flow, 0, sizeof(flow)); // 初始化流量为 0 42 43 44 45 int x,y,c; 46 47 for(int i = 1; i <= n; i++) 48 49 { 50 51 scanf("%d%d%d", &x,&y,&c); 52 53 cap[x][y] += c; // 因为可能会出现两个点有多条边的情况,所以需要全部加起来 54 55 } 56 57 int s = 1, t = m; // 第一个点为源点, 第 n 个点为汇点 58 59 60 61 queue<int> q; 62 63 int f = 0; // 总流量 64 65 66 67 for( ; ; ) // BFS找增广路 68 69 { 70 71 memset(a,0,sizeof(a)); // a[i]:从起点 s 到 i 的最小残量【每次for()时 a[] 重新清 0 因此同时可做标记数组 vis】 72 73 a[s] = INF; // 起点残量无限大 74 75 q.push(s); // 起点入队 76 77 78 79 while(!q.empty()) // 当队列非空 80 81 { 82 83 int u = q.front(); 84 85 q.pop(); // 取出队首并弹出 86 87 for(int v = 1; v <= m; v++) if(!a[v] && cap[u][v] > flow[u][v]) //找到新节点 v 88 89 { 90 91 p[v] = u; 92 93 q.push(v); // 记录 v 的父亲,并加入 FIFO 队列 94 95 a[v] = min(a[u], cap[u][v]-flow[u][v]); // s-v 路径上的最小残量【从而保证了最后,每条路都满足a[t]】 96 97 } 98 99 } 100 101 102 103 if(a[t] == 0) break; // 找不到, 则当前流已经是最大流, 跳出循环 104 105 106 107 for(int u = t; u != s; u = p[u]) // 从汇点往回走 108 109 { 110 111 flow[p[u]][u] += a[t]; //更新正向流 112 113 flow[u][p[u]] -= a[t]; //更新反向流 114 115 } 116 117 f += a[t]; // 更新从 s 流出的总流量 118 119 120 121 } 122 123 printf("%d\n",f); 124 125 } 126 127 128 129 return 0; 130 131 }