网络最大流(序)

网络最大流

前言

网络流是一种神奇的问题,在不同的题中你会发现各种各样的神仙操作。
而且从理论上讲,网络流可以处理所有二分图问题。
二分图和网络流的难度都在于问题建模,一般不会特意去卡算法效率,所以只需要背一两个简单算法的模板就能应付大部分题目了。
单纯只是为了A掉P3376 【模板】网络最大流,并且尽量效率高,把最最最板子的网络流整一遍。

概念

  • 网络流:一种类比水流的解决问题的方法。
  • 网络:可以理解为有一个源点和汇点的有向图。
  • 弧:也就是图中的有向边。
  • 弧的流量:简称流量,网络中的每条边都有一个流量,表示为 \(f(x,y)\),可以为负数。
  • 弧的容量:简称容量,网络中的每条边都有一个容量,表示为 \(c(x,y)\)
  • 源点:类比起点,能无限放出流量,表示为 \(S\)
  • 汇点:类比终点,能无限接受流量,表示为 \(T\)

  • 容量网络: 拥有源点和汇点且每条边都给出了容量的网络。
  • 流量网络: 拥有源点和汇点且每条边都给出了流量的网络。
  • 弧的残余容量:简称残余容量。在残量网络中每条边都会有一个残余容量。
    对于每条边,残余容量 = 这条边的容量 - 这条边的流量
  • 残量网络:拥有源点和汇点且每条边都有残留容量的网络。残量网络 = 容量网络 − 流量网络
    初始残量网络就是容量网络。

可以参照下图理解。
其中 \(c\) 表示容量,\(f\) 表示流量,\(flow\) 表示残余容量。

性质

  • 容量限制:\(\forall(x,y)\in E,f(x,y)\le c(x,y)\)。(很明显流量不能大于容量)
  • 流量守恒:\(\forall x\in V\)\(x\ne S,x\ne T\)\(\sum_{(u,x)\in E}f(u,x) = \sum_{(x,v)\in E}f(x,v)\)。(很明显流进来多少就要流出去多少)
  • 斜对称性:\(\forall (x,y) \in E,f(y,x)=-f(x,y)\)

EK

概念

  • 增广路:一条从残量网络中从 \(S\)\(T\) 的路径,路径上所有边的残余流量都为正。
  • 增广路定理:流量网络达到最大流当且仅当残量网络中没有增广路。
  • 增广路算法:基于增广路的一种算法,核心为 bfs 找最短的增广路,并按照一般增广方法处理。

算法流程

  1. 用 bfs 找到任意一条经过边数最少最短增广路,并记录路径上各边残留容量的最小值,因为木桶定律,容量最小的边不爆就行了。
  2. 根据残余流量最小值更新路径上边及其反向边的残留容量值。最大流加上其值。
  3. 重复流程 \(1,2\),直至无法找出增广路,结束算法。

算法解析

在实现算法的过程中,因为在残量网络跑增广路,所以只需记录最小的残余容量即可。
为了保证流量最大,所以要用到反悔的操作,在存图时建一条反向边权为零的边。

  • 如果用邻接表或链式前向星存边,表示边的编号的变量要初始化为 \(1\),因为这样做对于某条边 \(i\),其反边就是 \(i \operatorname{xor} 1\)。(链表的性质)
  • 如果用的是 vector 存边就只能记录反向边编号了。

当边 \((x,y)\) 的残余容量被用去了 \(f\) 时,其流量增加了 \(f\),残余流量应减少 \(f\),根据斜对称性,其反边 \((y,x)\) 的流量增加 \(-f\),残余流量增加 \(f\)
那么如果在以后找增广路时选择了这一条边,就等价于:将之前流出去的流量的一部分(或者全部)反悔掉了个头,跟随着新的路径流向了其它地方,而新的路径上在到达这条边之前所积蓄的流量 以及 之前掉头掉剩下的流量 则顺着之前的路径流了下去
同理,当使用了反向边 \((y,x)\) 的残留容量时也应是一样的操作。

图解

两次分别跑出一条增广路,用色笔画出。
图中只标出残余容量。

时间复杂度

\(O(nm^2)\)
因为使用 bfs 找到的必然是当前含边数最少的一条增广路,找一次需要 \(O(m)\) 的时间。
假如要利用反向边的话,必须走到一条边的尽头再往回走,这样一定比当前的最短路要大,所以是不会产生的,这样子我们就不用考虑容量改变的反向边。
每条增广路都有一个瓶颈,而两次增广的瓶颈不可能相同,所以增广路最多 \(m\) 条。
而增广路的长度是 \(\left[ 1,n \right]\),这就证明了 EK 的复杂度是 \(O(nm^2)\)
实际一般跑的挺快,一般能解决 \(\left[ 10^3,10^4 \right]\) 规模的网络。

代码

#include <bits/stdc++.h>
using namespace std;
int rd() {
    int w = 0, v = 1;
    char c = getchar();

    while (c < '0' || c > '9') {
        if (c == '-')
            v = -1;

        c = getchar();
    }

    while (c >= '0' && c <= '9')
        w = (w << 1) + (w << 3) + (c & 15), c = getchar();

    return w * v;
}
const int N = 2e4, INF = 2e9;
int fir[N], c = 1, n, m, st, ed, f[N], p[N], mi[N];
long long ans;
queue <int>q;
struct E {
    int v, nt, w;
} e[N];
void I(int u, int v, int w = 0) {
    e[++c].v = v;
    e[c].w = w;
    e[c].nt = fir[u];
    fir[u] = c;
}
bool bfs() {
    for (int i = 1; i <= n; i++)
        f[i] = 0;

    q.push(st), f[st] = 1;
    mi[st] = INF;

    while (!q.empty()) {
        int u = q.front(), V;
        q.pop();

        for (int i = fir[u]; i; i = e[i].nt)
            if (e[i].w && !f[V = e[i].v])
                mi[V] = min(mi[u], e[i].w), q.push(V), f[V] = 1, p[V] = i;
    }

    return f[ed];
}
void ek() {
    while (bfs()) {
        int x = ed;
        ans += mi[ed];

        while (x != st) {
            int i = p[x];
            e[i].w -= mi[ed];
            e[i ^ 1].w += mi[ed];
            x = e[i ^ 1].v;
        }
    }

    cout << ans;
}
int main() {
    n = rd(), m = rd(), st = rd(), ed = rd();

    for (int i = 1, u, v, w; i <= m; i++)
        u = rd(), v = rd(), w = rd(), I(u, v, w), I(v, u);

    ek();
}

Loj上的AC记录
洛谷上的AC记录

Dinic

更快的网络流算法,同时也是应用非常广泛的网络流算法,一般情况下不会被卡。

时间复杂度

相比于 EK 的 \(O(nm^2)\),Dinic 的 \(O(n^2m)\)稠密图上优势很大
用于二分图匹配时,复杂度为 \(O(m\sqrt n )\)

算法讲解

首先,EK 跑得慢原因很容易想到,一次 bfs 可能要遍历整张图,却只能找到一条增广路。
而 Dinic 解决方法就是多路增广
具体实现方法是两步:

  1. 跑 bfs 对网络分层。
  2. 用 dfs 跑增广路。

用下面的图作为例子。

在跑 bfs 时,相比于 EK,不记录路径,只对所有点分层。
具体操作看 bfs 部分代码:

bool bfs(){
	for(int i=1;i<=n;i++)d[i]=0;
	q.push(st);d[st]=1;
	while(!q.empty()){
		int u=q.front(),V;q.pop();
		for(int i=fir[u];i;i=e[i].nt)
			if(e[i].w&&!d[V=e[i].v])
				d[V]=d[u]+1,q.push(V);
	}return d[ed];
}

分层后的状况:

蓝色数字是点的层数。
分层是为了保证找到的路径是在最短增短路上。
比如下面的图(括号内的数是层数)

分层之后就不会走那条长路了。

bfs 分层之后的下一步是 dfs 增广。
还是用之前那张图作为例子:

分层之后 dfs 跑增广路。
比如,第一条增广路跑 \(S \Longrightarrow 1 \Longrightarrow 4 \Longrightarrow T\)
相应的流量发生变化,在图中已经标出。

很明显,图中还有增广路,所以可以继续使用第一次 bfs 的标号跑增广路。
第二条增广路是 \(S \Longrightarrow 1 \Longrightarrow 5 \Longrightarrow T\),如图:

还有第三条增广路:\(S \Longrightarrow 1 \Longrightarrow 3 \Longrightarrow T\)

最后是第四条增广路: \(S\Longrightarrow 2\Longrightarrow 3 \Longrightarrow T\)

综上,一次 dfs 能找到多条增广路,这就是 dinic 的高效原因。
然后就可以把 dinic 的代码写出来了。

#include <bits/stdc++.h>
using namespace std;
int rd() {
    int v = 1, w = 0;
    char c = getchar();

    while (c < '0' || c > '9') {
        if (c == '-')
            v = -1;

        c = getchar();
    }

    while (c >= '0' && c <= '9')
        w = (w << 1) + (w << 3) + (c & 15), c = getchar();

    return w * v;
}
const int N = 2e4, INF = 2e9;
int c = 1, fir[N], n, m, st, ed, d[N];
queue <int>q;
long long ans;
struct E {
    int v, nt, w;
} e[N];
void I(int u, int v, int w = 0) {
    e[++c].v = v;
    e[c].w = w;
    e[c].nt = fir[u];
    fir[u] = c;
}
bool bfs() {
    for (int i = 1; i <= n; i++)
        d[i] = 0;

    d[st] = 1;
    q.push(st);

    while (q.size()) {
        int u = q.front(), V;
        q.pop();

        for (int i = fir[u]; i; i = e[i].nt)
            if (!d[V = e[i].v] && e[i].w)
                q.push(V), d[V] = d[u] + 1;
    }

    return d[ed];
}
int dfs(int u, int fl) {
    if (u == ed)
        return fl;

    int V, tmp = 0;

    for (int i = fir[u]; i; i = e[i].nt)
        if (e[i].w && d[V = e[i].v] == d[u] + 1 && (tmp = dfs(V, min(fl, e[i].w)))) {
            e[i].w -= tmp, e[i ^ 1].w += tmp;
            return tmp;
        }

    return 0;
}
void dinic() {
    int g;

    while (bfs()) {
        while (g = dfs(st, INF))
            ans += g;
    }

    cout << ans << endl;
    return ;
}
int main() {
    n = rd(), m = rd(), st = rd(), ed = rd();

    for (int i = 1, u, v, w; i <= m; i++)
        u = rd(), v = rd(), w = rd(), I(u, v, w), I(v, u);

    dinic();
    return 0;
}

Loj 的提交记录
洛谷的提交记录
都收获了 TLE。
因为单路增广的 dinic 的时间复杂度是不能保证的,所以要对此进行优化。

多路增广

一次 bfs 会找到 \(\left[ 1,m \right]\) 条增广路,大大减少了 bfs 次数,但 dfs 更新路径上的信息仍是在单路增广,效率相较于 EK 并没有多大变化。

用一个变量存经过路径上的最小流量,在 dfs 找到一条增广路之后不直接返回,回溯更新之前的信息,直至流量达到上限。

代码只放了有改变的部分。

int dfs(int u, int fl) {
    int V, f;

    if (u == ed)
        return fl;

    for (int i = fir[u]; i; i = e[i].nt)
        if (e[i].w && d[V = e[i].v] == d[u] + 1 && (f = dfs(V, min(fl, e[i].w)))) {
            e[i].w -= f;
            e[i ^ 1].w += f;
            return f;
        }

    return 0;
}
void dinic() {
    while (bfs())
        ans += dfs(st, INF);
//因为可以多路同时增广,所以每个分层图只要跑一次 dfs 即可
    cout << ans << endl;
    return ;
}

To be continued

posted @ 2022-05-20 21:30  AIskeleton  阅读(40)  评论(0编辑  收藏  举报