zoukankan      html  css  js  c++  java
  • 「最大流」学习笔记

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

    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);
    }
  • 相关阅读:
    转 | 禁忌搜索算法(Tabu Search)求解带时间窗的车辆路径规划问题详解(附Java代码)
    Branch and price and cut求解传统VRP问题以及VRPTW问题
    标号法(label-setting algorithm)求解带时间窗的最短路问题(ESPPRC)
    运筹学从何学起?如何快速入门精确式算法?
    转 | 模拟退火算法(SA)和迭代局部搜索(ILS)求解TSP的Java代码分享
    用Python画论文折线图、曲线图?几个代码模板轻松搞定!
    45. 截取“测试数据”后面的内容
    44. 更改oracle字符集编码american_america.zh16gbk 改为 SIMPLIFIED CHINESE_CHINA.ZHS16GBK
    18. 浏览器关闭页面时弹出“确定要离开此面吗?”
    6. concat_ws用法
  • 原文地址:https://www.cnblogs.com/qixingzhi/p/9413739.html
Copyright © 2011-2022 走看看