这里总结一下几种最大流算法
1.EK算法
EK算法应该是最大流中最简单的了,但刚开始理解也花了不少工夫
EK算法基于增广路。它的思想是,每一次通过BFS不停寻找增广路,找到以后增广,直到找不到为止
残量
一条边的残量等于该边的容量减去该边当前的流量,为了方便,我们可以建立残量网络。这样每条边只需要对应一个权值即可。
反向边
给程序一个反悔的机会。因为一个点出发边的搜索顺序完全取决于读入的顺序,所以难免会运气不好走出一条路使下一次无法增广,而实际上如果不那么走是有增广路的。因此引入反向边的概念,当第一次运气不好走过一条边时,下一次可以反的走回来,这样一条边走去走来就抵消了,相当于没走,也就是反悔了
问题的统一
从u到v流去3,再从v到u流回5,实际上等价于v到u流回-2。因此,流量$f(u,v) = k$等价于$f(v,u) = -k$
具体的算法流程及内容相关:
1.建图
对于每一条边,建立它本身与反向边。
它本身的容量就设为c,流量初始化为0。 反向边的容量设为0,流量也为0
每条边在残量网络中对应正反两条边,当容量为c,当前流量为f时,正向边残量为$c-f$,反向边残量为$0-(-f)=f$,容易看出正反向边始终满足残量之和等于容量
假设当前一条边经过增广使流量增加了$Δf$,则正向边残量为$c-(f+Δf)$,反向边残量应当为$f+Δf = 0-(-f-Δf)$才能满足容量的守恒。由此可知,当流量增加$Δf$时,反向边的流量应当减去$Δf$
2.增广
利用BFS进行增广
基于已经构建好的残量网络(也就是容量-流量),BFS出任意一条路劲,使其能够增广。这里的增广就好像一个瓶颈,能增广多少取决于残量最小的那条边。
因此我们可以记录一个数组a,a[i]表示从i到源点经过的残量的最小值。如果$a[T]>0$,则意味着存在一条增广路
我们不停进行BFS,直到$a[T]==0$。注意a[S]应当为INF,用来打擂最小值
3.累积
注意我们不能再BFS增广的过程中对边的流量进行修改,因为BFS几乎会经过所有的边,而只有一条路径作为增广路。所以我们需要像最短路输出方案那样记录更新的pre数组,最后由汇点T往前一一处理经过的边(正向加反向减)即可。这里还有一个小技巧:我们可以让num_edge从0开始累积,由于正反向的两条边是连续add的,所以边的编号刚好是x与x^1. 这样访问起来也非常方便
注意,这不是单纯地输出路径,而是需要对路径上的边权做出修改。无疑不能仅仅存点的编号为pre,而是边的编号,并且还需要在边的属性中加入from这一项
EK算法复杂度最坏为$O(nm^2)$
inline void MaxFlow_EK(){ int ans = 0; for(;;){ memset(a, 0, sizeof(a)); while(!q.empty()) q.pop(); //各数组预先清空 q.push(S); //源点入队 a[S] = INF; //a值先设为INF,以便打擂最小值 int u,v; while(!q.empty()){ u = q.front();q.pop(); for(int i = first[u]; i != -1; i = nxt[i]){ v = to[i]; //a数组在此处充当vis数组,没有访问过的点a必定为0 if(!a[v] && cap[i]>flow[i]){//cap[i]>flow[i]也就是残量大于0 pre[v] = i; //pre[v]记录为u->v的边的编号 a[v] = Min(a[u], cap[i]-flow[i]); //打擂最小值,也就是找到瓶颈 q.push(v);//v入队 } } if(a[T] > 0) break; //意味着已经找到增广路,可以停止BFS } if(!a[T]) break;//无法找到增广路了,跳出大的循环 for(int i = T; i != S; i = from[pre[i]]){ flow[pre[i]] += a[T]; //正向边,终点为i。正向边的流量增大a[T] flow[pre[i]^1] -= a[T]; //相反,反向边的流量应当减小a[T] } ans += a[T];//每一次增广累积答案 } printf("%d", ans); }
2. Dinic算法
在讨论Dinic算法之前,先来看最短增广路算法。Dinic基于该算法,事实上Dinic是连续最短增广路算法
最短增广路算法的步骤如下:
1. 构建层次图
先对原图从源点开始BFS,处理出每个点到S的距离(这里每条边的权值当做1,也就可以理解为每个点到S最少经过几条边)。这个距离记为这个点的层次。
2. BFS求解最大流
将同层次的点归于一组,并规定每一次只能走层次较大的(最多只能大1,想一想,为什么)。每一次BFS的过程中找到增广路,并进行增广
3. 重新构建层次图
当找不到增广路后,去除所有饱和的边,重新构建层次图,重新BFS找增广路
为什么这样会优呢?因为每一次构建层次图,都是让源点到汇点的增广路同时为最短路。每一次重构层次图意味着增广路的最短路至少增加了1. 因此重构不会超过$n-1$次
再回过头来看Dinic,Dinic算法的优化就在于不要用BFS每次求解一条增广路,这样非常慢。Dinic算法的思想是多路增广,即利用DFS一次性求解多条增广路
下面来介绍Dinic算法的步骤
1. 构建层次图
与最短增广路算法一样
2. DFS多路增广
每一次在DFS的时候就可以顺带减去当前边的流量,因为是深度优先,所以在回溯的时候已经知道了流量的消耗。具体原理还是和EK一样的
3. 重新构图进行多路增广
也和最短增广路算法一样
以上是朴素的Dinic算法,我们可以尝试对他进行优化
优化(一):当前弧优化
所谓当前弧优化就是指在同一次DFS中,记录每个点已经访问过的边,下次从没有访问的边开始访问
这有点像记忆化搜索,由于是深度优先搜索,所以如果一条边已经搜索过了,那么从它出发的经过它的所有增广路肯定都已经增广了(不然不会回溯到现在),所以这条边再DFS下去毫无价值,因此可以忽略。所以反应到程序里就是每一个点搜过的边都不要再搜,也就是当前弧优化
有一个地方是不是矛盾了?既然是深度优先搜索,那么一个点搜过了所有边肯定也会被遍历过,为什么还需要当前弧优化呢?因为我们有一个重要的跳出语句(也就是下一条优化):我们是有剩余流量的,参数a表示这个点出发总共还有多少从前面运输过来的流量,如果增广前三条边导致剩余流量用光,那么只能被迫跳出。因此当前弧优化就在这里起到了作用
优化(二):剩余流量为0时及时跳出
既然没有剩余流量了,跳出即可(如上)
Dinic算法复杂度最坏$O(n^2m)$
inline bool BFS(){ memset(vis, 0, sizeof(vis)); memset(level, 0, sizeof(level)); while(!q.empty()) q.pop(); q.push(S); level[S] = 1;//层次 vis[S] = 1; int u,v; while(!q.empty()){ u = q.front(); q.pop(); for(int i = first[u]; i!=-1; i = nxt[i]){ v = to[i]; if(!vis[v] && cap[i]>flow[i]){//仅访问残量>0的弧,这样才能够用是否到达判断是否存在增广路 vis[v] = 1; level[v] = level[u]+1; q.push(v); } } } return vis[T];//能访问T意味着存在至少一条增广路 } int DFS(int u, int a){ //a表示当前点总共可以发出多少流量 if(u == T || a == 0) return a;//如果已经到达汇点,或是无法发出任何流量,return int ans = 0, v, _f; for(int& i = cur[u]; i!=-1; i = nxt[i]){ //注意这里的i有一个&符号,这样cur[u]就会随i的变化而变化,自动更新访问到了哪里 v = to[i]; if(level[u]+1 == level[v] && cap[i]-flow[i] > 0){//访问下一个层级的,并且残量>0 _f = DFS(v, Min(a, cap[i]-flow[i]));//此时进行DFS,_f就表示从这个点一直到汇点所能够增广的大小 这里之所以要取Min是因为有可能a不足以流满 a -= _f;//由于得到的这_f需要增广,于是剩余流量减去_f flow[i] += _f; flow[i^1] -= _f;//正向边反向边处理 ans += _f;//答案累积增广的总量 if(a == 0) break;//判断如果没有剩余流量则跳出,这也是当前弧优化存在的意义 } } return ans; } inline void Dinic(){ int ans = 0; while(BFS()){//重新建分层图以后能够到达汇点,意味着存在增广路 for(int i = 1; i <= N; ++i) cur[i] = first[i]; ans += DFS(S, INF);//源点可以发出无限的流量 } printf("%d", ans); }