「最大流」学习笔记

这里总结一下几种最大流算法

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);
}
原文地址:https://www.cnblogs.com/qixingzhi/p/9413739.html