• 网络流各算法超详细带源码解析


    网络最大流

    链接:洛谷日报EK 博客1 博客2 洛谷日报Dinic 洛谷日报ISAP和HLPP

    FF算法:朴素的算法

    Ford-Fulkerson's Algorithm

    【名词】增广路:一条从起点走到终点的道路,其上的剩余流量的最小值大于0,能够为答案做出贡献。

    【动词】增广:对一条增广路进行增广,就是求出这条路上的剩余流量最小值Min,然后给这条路上的每一条路减去Min,同时给它们的反向加上Min。

    FF算法的原理就是:随机找一条走到从源点S走到终点T的增广路,然后对这条路增广。所以你在走的时候,要判断这条边的剩余流量是否已经耗到了0,耗到0就不走了。

    很简单,就是一个dfs就完事了。在dfs中维护路上的最小值,在到达t后回溯的过程中对边进行修改。函数返回值是这条路的最小值,而最终答案则是用全局变量保存,在每次到达t点的时候加进答案。

    因为每一次增广都会增加答案,而最大流显然是有限的,所以这样的增广也只会进行有限次——只是会有很多次。真的很多很多。

    最经典的反例就是这个。

    1    --->  > 2
               /      
             /         
          >3------->>4
    

    在这个图上用FF算法可能会跑200000次。而且,只要增加边的权值就可以增加你走的次数,而这个权值完全可以增加到(10^9)。所以FF几乎是没人用的。

    EK算法:BFS优化

    Edmond-Karp's Algorithm

    EK的算法是在FF的基础上优化的,它的基本思想就是:通过从S出发广搜,优先走最短的路。

    每次广搜时记录from数组和fromp数组,记录来源的点,和来源的边的编号。当你广搜搜到了T点的时候,就可以立即停手,然后返回。与FF相似地,剩余流量为0的边我也是不会走的。

    接着,你就可以从T点出发,沿着from一路回到S点,这就是一条增广路。

    当你BFS搜不到T点的时候,就说明已经没有增广路了,那么你就可以返回了。

    每一个人都会觉得这有点浪费。“我BFS搜遍了整个图,结果只有一条路能用!”所以,DinIc就产生了。

    EK算法的时间是(O(NM^2))

    Dinic算法:记录到S距离

    Dinic的广搜多了一点东西:你要保存每一个点到S点的最短距离dep[i]。(其实相当于层级)

    然后,你在dfs的时候就严格地要求只能从u点走到dep比u大1的点。这样就达到了“增广路是最短路”的目的。优化之处在于,你是可以利用这一次BFS的成果,进行多次dfs的;每次dfs能且只能处理一条最短路(是不是有点像FF算法)。这样BFS的效用就被增大了。

    dfs的内容就是在找到t点之后一路返回,一边返回一边修改边的剩余流量。

    Dinic算法的时间是(O(N^2M)),在稀疏图上和EK差不多,但是到了稠密图上就快很多。

    在这个程度上,优化其实就已经很明显了。然而Dinic还可以加两个Buff(优化)。

    多路增广:真·DFS

    假设你从S点走到当前的u点的路上的剩余流量最小值是Rest,那么你可以在枚举dfs出边的时候,走完一个支线之后再在下一个支线走,直到Rest被耗完或者出边被走完了。

    完成这个优化只需要在原dfs中做一些不大的修改。

    注意一件事情:虽然是多路增广,但是在一次广搜之后还是要进行多次的深搜的。不能保证一次深搜就能用完一次BFS的成果。判断DFS是否已经足够的方法就是:看DFS能否在那个dep的限制下走到T。(这一段待验证)

    当前弧优化:不做无用功

    在这个图上,在两次BFS之间的多次dfs中,同一个点u可能可以通过不同的几条路到达。

    在以前的深搜中,我已经把前面的几条边的未来可能用的流量用完了,这条边已经成了”废边“,所以就算搜这几条边也只能获得0的收益,而且这些多余的深搜还会蛮耗时间。

    所以,我们可以用一个cur数组临时代替tu数组,让下次再来的时候直接从cur开始。cur表示的是离tu最近的还没有增广完的边。

    什么时候修改cur呢?最好的方法就是:在v = to[p]的下面一句就加上cur[u]=p

    // 在这里整合一下加了两个Buff的Dinic的深搜
    int dfs(int u, int low) {
        int left = low;
        if(u == t) {
            flag = true; // 代表成功地走到了T节点,可以继续dfs。如果flag = false,那么就需要重新广搜。 
            Maxflow += low;
            return low;
        }
        for(int v, p = cur[u]; p; p = nxt[p]) {
            v = to[p];
            cur[u] = p;
            if(dep[v] == dep[u] + 1 && f[p] > 0) { // f[p]就是边的流量
                int gone = dfs(v, min(left, f[p]));
                f[p] -= gone;
                f[p ^ 1] += gone;
                left -= gone;
                if(left <= 0) break;
            }
        }
        return low - left;
    }
    

    补充:Dinic跑二分图匹配比匈牙利算法还快得多。

    补充:如果输入数据中一条边的正反两条边都有,那么在这两个点之间就会有4条边。

    补充:CSP是不会卡Dinic的。

    补充:要用vis。不然会导致在一条边上反复走。

    ISAP算法:动态修改分层

    Improved Shortest Augumenting Path

    闲的没事的科学家们对于Dinic还不满足,发明了ISAP。

    先来算法步骤:

    1. 从t到s跑一遍bfs,标记深度。

    2. 从s到t跑dfs,和Dinic类似,只是当一个点的所有出边都被耗完后,如果从上一个点传过来的flow比该点的used大(对于该点当前的深度来说,该点在该点以后的路上已经废了),则把它的深度加1。此时判断如果出现断层(某个深度没有点),把源点S的深度标记为n+1,结束算法。

    3. 如果操作2没有结束算法,重复操作2

    原理:

    每个点的深度随着dfs的进行而不断提高。当所有边走完后,这个点就成了“废点”。

    注意:

    1. 广搜时没有剩余流量>0的限制。
    2. 深搜的时候,前面部分与Dinic没有区别,只在函数最后进行修改深度的操作。
    3. 要用桶来统计并维护每个深度的点的个数,以快速判断是否出现断层。
    4. ISAP的主函数void ISAP()中唯一的循环是while(dep[s] > n) dfs(s, INF);
    5. 可以使用当前弧优化,但是不知道能不能多路增广。我自己推不出来,高二也没有详细了解。

    时间复杂度仍然是(O(N^2M)),但是比Dinic快。

    预流推进算法

    基本思想就是:源点有INF的水,然后往每一个点灌尽量多的水(称为“推流”),一直到最后。思想很简单,但是实际上有很多麻烦的事情。

    预留推进算法的思想是:

    1. 先假装s有无限多的余流,从s向周围点推流(把该点的余流推给周围点,注意:推的流量不能超过边的容量也不能超过该点余流),并让周围点入队。注意:s和t不能入队

    2. 不断地取队首元素,对队首元素推流

    3. 队列为空时结束算法,t点的余流即为最大流。

    上述思路是不是看起来很简单,也感觉是完全正确的?

    但是这个思路有一个问题,就是可能会出现两个点不停地来回推流的情况,一直推到TLE。

    怎么解决这个问题呢?

    给每个点一个高度,水只会从高处往低处流。在算法运行时, 不断地对有余流的点(包括推出去的点和被推流的点)更改高度,改为它推出去了所有点中高度最高的点的高度+1(如果是被推流的点就+1) ,直到这些点全部没有余流为止。

    为什么这样就不会出现来回推流的情况了呢?

    当两个点开始来回推流时,它们的高度会不断上升,当它们的高度大于s时,会把余流还给s。

    所以在开始预流推进前要先把s的高度改为n(点数),免得一开始s周围那些点就急着把余流还给s。

    这个预留推进算法相当慢。我们学这个的目的是为下面的东西做铺垫。

    HLPP:升级版预流推进

    算法步骤

    1.先从t到s反向bfs,使每个点有一个初始高度

    2.从s开始向外推流,将有余流的点放入优先队列

    3.不断从优先队列里取出高度最高的点进行推流操作

    4.若推完还有余流,更新高度标号,重新放入优先队列

    5.当优先队列为空时结束算法,最大流即为t的余流

    与基础的余流推进相比的优势:

    通过bfs预先处理了高度标号,并利用优先队列(闲着没事可以手写堆)使得每次推流都是高度最高的顶点,以此减少推流的次数和重标号的次数。

    优化:

    和ISAP一样的gap优化,如果某个高度不存在,将所有比该高度高的节点标记为不可到达(使它的高度为n+1,这样就会直接向s推流了)。

    代码非常恐怖,我没有了打代码的勇气。

    时间复杂度$ O(n^2sqrt m)$,常数较大,导致随机数据下还没有ISAP快。ISAP应该是最牛的。

    最小费用最大流

    每一条边有了单位流量的花费C[i] 。

    看起来好像很毒瘤,就是那种  NOI/NOI+/CTSC  的题目。

    (实际上是  提高+/省选-   P3381 【模板】最小费用最大流

    但是其实很简单。把bfs替换成最短路算法就可以了。需要使用SPFA。(这里的SPFA与正常SPFA的区别就是:剩余流量为0的边是不走的。所以就不会出现负环了。)

    同时,DFS的要求dep[u] + 1 == dep[v]就改变为了dist[u] + cost[p] == dist[v]。两者的追求都是走最短路。

    当你要修改边的剩余流量的时候,同时计算花费就行了。

    注意:一条边的反向边的费用是它的相反数。正因为相反数的存在,所以只能用SPFA而不能用Dij。

    注意:因为这是费用流,所以边的代价可能为0,。这样就会出现dfs时在两个节点之间来回跑的现象。这样,就必须要给每一个点打上不会因为dfs的return而false的vis标记,防止去同一个点两次(但是特殊地,去t点又可以去多次)。正是因为这个限制,所以我们就算有了多路增广,一次bfs之后也不能只dfs一次,不然不够。

    总结:

    1. 边的反向边的费用是边的费用的相反数。
    2. 每次深搜只能搜每个点一次,但是可以搜t点多次。
    3. 使用SPFA来求最短路(洛谷题解中有一位大佬搞出了Dij的做法,很神仙)

    20191111版代码(Dinic)

    /* for vjudge
    ID: wangyuxi20040901
    TASK:  ()
    LANG: C++
    DATE: 20191111 17:24:47
    *///using CRLF, UTF-8
    
    #include <bits/stdc++.h>
    #define pr printf
    #define F(i, j, k) for(register int i = j, kkllkl = k; i <= kkllkl; ++i)
    #define G(i, j, k) for(register int i = j, kkllkl = k; i >= kkllkl; --i)
    #define clr(a) memset((a), 0, sizeof(a))
    #define rg register
    using namespace std;
    typedef long long ll;
    
    #define isd(x) (('0' <= (x) && (x) <= '9') || (x) == '-')
    int rd() {
        int ans = 0, sign = 1; char c = getchar();
        while(!isd(c)) c = getchar();
        if(c == '-') sign = -1, c = getchar();
        while(isd(c)) ans = (ans << 3) + (ans << 1) + c - '0', c = getchar();
        return sign == 1 ? ans : -ans;
    }
    
    #define OJ
    // #define DEBUG
    
    /* ------------------------CSYZ1921--------------------------- */
    
    const int N = 5005, M = 50005, INF = 0x3f3f3f3f;
    int n, m, s, t;
    int tu[N], to[M << 1], nxt[M << 1], cost[M << 1], f[M << 1], tot = 1;
    int cur[N];
    bool vis[N];
    int Maxflow, Mincost; // 最终答案
    
    void cnct(int u, int v, int w, int c) {
        to[++tot] = v;
        cost[tot] = c;
        f[tot] = w;
        nxt[tot] = tu[u];
        tu[u] = tot;
    }
    
    queue<int> Q; bool inque[N]; int dist[N];
    bool SPFA() {
        F(i, 1, n) dist[i] = 0x3f3f3f3f, inque[i] = false, cur[i] = tu[i];
        Q.push(s);
        dist[s] = 1;
        inque[s] = true;
        while(!Q.empty()) {
            int u = Q.front(); Q.pop();
            inque[u] = false;
            for(int v, p = tu[u]; p; p = nxt[p]) {
                v = to[p];
                if(dist[v] > dist[u] + cost[p] && f[p] > 0) {
                    dist[v] = dist[u] + cost[p];
                    if(!inque[v]) Q.push(v), inque[v] = true;
                }
            }
        }
        return dist[t] != INF;
    }
    
    int dfs(int u, int low) {
        int left = low;
        vis[u] = true;
        if(u == t) {
            Maxflow += low;
            return low;
        }
        for(int v, p = cur[u]; p; p = nxt[p]) {
            v = to[p];
            if((!vis[v] || v == t) && dist[v] == dist[u] + cost[p] && f[p]) {
                int gone = dfs(v, min(left, f[p]));
                left -= gone;
                f[p] -= gone; Mincost += gone * cost[p];
                f[p ^ 1] += gone;
                if(left <= 0) {
                    break;
                }
            }
        }
        return low - left;
    }
    
    void Dinic() {
        while(SPFA()) {
            vis[t] = true;
            while(vis[t]) {
                clr(vis);
                dfs(s, INF);
            }
        }
    }
    
    int main() {
        n = rd(), m = rd(), s = rd(), t = rd();
        F(i, 1, m) {
            int u = rd(), v = rd(), w = rd(), c = rd();
            cnct(u, v, w, c);
            cnct(v, u, 0, -c);
        }
    
        Dinic();
        pr("%d %d
    ", Maxflow, Mincost);
        return 0;
    }
    /*
    ------------------------------------------------------------
    g++ -o P3381【模板】最小费用最大流 P3381【模板】最小费用最大流.cpp
    ./P3381【模板】最小费用最大流
    4 5 4 3
    4 2 30 2
    4 3 20 3
    2 3 20 1
    2 1 30 9
    1 3 40 5
    >>
    50 280
    */
    

    活用

    洛谷日报:有限制的图上最短(长)路

  • 相关阅读:
    20182316胡泊 实验5报告
    20182316胡泊 第6周学习总结
    20182316胡泊 第5周学习总结
    20182316胡泊 实验4报告
    20182316胡泊 实验3报告
    20182316胡泊 第4周学习总结
    20182316胡泊 第2,3周学习总结
    实验2报告 胡泊
    《数据结构与面向对象程序设计》第1周学习总结
    实验1报告
  • 原文地址:https://www.cnblogs.com/lightmain-blog/p/14977104.html
Copyright © 2020-2023  润新知