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

网络最大流

链接:洛谷日报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^2\sqrt 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\n", 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
*/

活用

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

posted @ 2021-07-06 15:20  lightmain  阅读(392)  评论(0编辑  收藏  举报