【学习笔记】网络最大流

· · 算法·理论

更好的阅读体验

网络流简介 & 相关概念

网络流,属于图论算法,模型为一张有向无环图,然后各位需要知道以下要点:

网络最大流

首先我们再明确一点,一张网络图的流量定义某种路径设计方案下汇点的流量总和。

那么显而易见,网络最大流就是所有路径设计方案中流量总和最大的那种。

首先相信在座的各位同学肯定能够想到我们可以每次 dfs 找一条路径从 ST 然后找路径流量最小值(路径上没有任何一条边流量为 0),这条路径也被称为增广路。

每次找到一条增广路,得到路径上最小流量后修改每条边的流量,直到再也找不到增广路后该网络流量来到最大。

但很明显,这个做法时间复杂度会爆炸,而且有的时候我们需要反悔(有可能存在一条更优的增广路遍历顺序),考虑如何优化。

EK 算法

EK 算法的核心就是利用初始流量为空的反向边进行一个悔的反,首先通过 bfs 找到经过边数最少的增广路,然后找出最小的流量 v,此时我们让反向边加上 v,原始边减去 v,这样就算是完成了一条增广路的修改,如此往复知道找不到增广路,最坏复杂度 O(nm^2)

Dinic 算法

你发现 EK 算法在稠密图中直接寄掉,那么我们需要进一步的优化。

EK 算法每次会遍历整个残量网络但是最后只会找出一条增广路,那么我们有没有办法一次性找多条呢?

首先我们用按照每个点到 S 的距离分层,随后 bfs 判断是否存在到 T 的增广路,如果存在我们按照层级 dfs,每次只能从上一层遍历到下一层,之后跟 EK 同理的操作,直到当前层出发的所有到 T 的增广路遍历完,这样做可以做到每次找出 1 \sim m 条增广路,效率提升很多,同时 dfs 需要进行一点剪枝,残余流量为 0 则不再继续向更深层遍历,如果出现最小流量为 0 的情况则将当前点置为 inf 层,这样之后就不会浪费时间遍历已经处理完的增广路。

然后再引入当前弧优化,我们发现我们仍旧会遍历已经被榨干的边,那么我记录从当前点出发没有被榨干的边从其开始向下遍历,这样也会省去很多时间,最坏复杂度 O(n^2m)。(业内规定不卡 Dinic)。

对于二分图 Dinic 的复杂度为 O(n\sqrt{m})

只需要掌握以上两种算法可以解决大部分问题(另外还有两种更快的算法)。

P3376 【模板】网络最大流

#include <queue>
#include <cstring>
#include <iostream>
#include <algorithm>
#define int long long

using namespace std;
constexpr int N = 1e4 + 10;
constexpr int inf = 0x7f7f7f7f;

int n, m, S, T, idx, ans;
int head[N], cur[N], depth[N];

struct graph {
    int to;
    int nxt;
    int val;
} E[N << 1];

void addedge (int u, int v, int w) {
    E[idx].to = v;
    E[idx].val = w;
    E[idx].nxt = head[u];
    head[u] = idx++;
}

bool bfs() {
    memset (depth, 0, sizeof(depth));
    queue<int> q;
    q.emplace(S);
    depth[S] = 1;
    while (!q.empty()) {
        int now = q.front(); q.pop();
        for (int i = head[now]; ~i; i = E[i].nxt) {
            int to = E[i].to, val = E[i].val;
            if (val && !depth[to]) {
                depth[to] = depth[now] + 1;
                q.emplace(to);
            }
        }
    }
    return !depth[T] ? false : true;
}

int dfs (int now, int flow) {
    if (now == T) return flow;
    int sum = 0;
    for (int i = cur[now]; ~i; i = E[i].nxt) {
        cur[now] = i;
        int to = E[i].to, val = E[i].val;
        if (val && depth[to] == depth[now] + 1) {
            int tmpv = dfs (to, min(flow, val));
            E[i].val -= tmpv;
            E[i ^ 1].val += tmpv;
            sum += tmpv, flow -= tmpv;
            if (!flow) break;
        }
    }
    if (!sum)
        depth[now] = 0;
    return sum;
}

void dinic() {
    while (bfs()) {
        memcpy (cur, head, sizeof(cur));
        ans += dfs (S, inf);
    }
    return;
}

signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    cin >> n >> m >> S >> T;
    memset (head, -1, sizeof(head));
    for (int i = 1; i <= m; i ++) {
        int u, v, w;
        cin >> u >> v >> w;
        addedge (u, v, w), addedge (v, u, 0);
    }
    dinic();
    cout << ans << '\n';
    return 0;
}

下面来几道例题。

Ex.1 P3254 圆桌问题

记源点汇点为 S,T

看上去很二分图啊,直接连 s \to i \in [1,m] 的边权值为 r_i,之后再连 i \in [m+1,m+n] \to T 权值为 c_i 的边,再建完全二分图跑最大流,构造方案的话输出 \forall i \in [1,m] 的出点。

#include <queue>
#include <cstring>
#include <iostream>
#include <algorithm>
#define int long long

using namespace std;
constexpr int N = 1e5 + 10;
constexpr int inf = 0x3f3f3f3f3f3f3f3f;

int m, n, idx, sum, S, T;
int head[N], cur[N], dis[N];

struct graph {
    int to;
    int val;
    int nxt;
} E[N];

void addedge (int u, int v, int w) {
    E[idx].to = v, E[idx].val = w, E[idx].nxt = head[u], head[u] = idx++;
    E[idx].to = u, E[idx].val = 0, E[idx].nxt = head[v], head[v] = idx++;
}

bool bfs() {
    queue<int> q;
    memcpy (cur, head, sizeof(int) * (T + 1));
    memset (dis, 0x3f, sizeof(int) * (T + 1));
    q.emplace(S), dis[S] = 0;
    while (!q.empty()) {
        int u = q.front(); q.pop();
        for (int i = head[u]; ~i; i = E[i].nxt) {
            int v = E[i].to, w = E[i].val;
            if (w && dis[v] == inf) {
                dis[v] = dis[u] + 1;
                q.emplace(v);
            }
        }
    }
    return dis[T] != inf;
}

int dfs (int u, int flow) {
    if (u == T)
        return flow;
    int res = 0;
    for (int i = cur[u]; ~i; i = E[i].nxt) {
        cur[u] = i;
        int v = E[i].to, w = E[i].val;
        if (w && dis[v] == dis[u] + 1) {
            int tmpv = dfs (v, min(w, flow));
            E[i].val -= tmpv;
            E[i ^ 1].val += tmpv;
            res += tmpv;
            flow -= tmpv;
            if (!flow) break;
        }
    }
    if (!res)
        dis[u] = inf;
    return res;
}

bool dinic() {
    int ans = 0;
    while (bfs()) 
        ans += dfs(S, inf);
    return ans == sum;
}

signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);

    cin >> m >> n, T = n + m + 1;
    memset (head, -1, sizeof(int) * (T + 1));
    for (int i = 1, w; i <= m; i ++)
        cin >> w, addedge (S, i, w), sum += w;
    for (int i = 1, w; i <= n; i ++)
        cin >> w, addedge (i + m, T, w);
    for (int i = 1; i <= m; i ++) {
        for (int j = 1; j <= n; j ++) 
            addedge (i, m + j, 1);
    }
    if (dinic()) {
        cout << "1\n";
        for (int u = 1; u <= m; u ++) {
            for (int i = head[u]; ~i; i = E[i].nxt) {
                if (E[i].to != S && !E[i].val)
                    cout << E[i].to - m << ' ';
            }
            cout << '\n';
        }
    } else {
        cout << "0\n";
    }
    return 0;
}

Ex.2 P2762 太空飞行计划问题

超级源连实验,流量为赞助商费用,仪器连超级汇,流量为配置仪器费用,中间实验连仪器,流量 inf,然后跑最大流,方案的话直接输出实验和仪器哪些被增广过。

#include <queue>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define int long long

using namespace std;
constexpr int N = 1e3 + 10;
constexpr int inf = 0x3f3f3f3f3f3f3f3f;

int m, n, idx, S, T, ans;
char buf[505];
int head[N], cur[N], dis[N];

struct graph {
    int to;
    int val;
    int nxt;
} E[N << 1];

void addedge (int u, int v, int w) {
    E[idx].to = v, E[idx].val = w, E[idx].nxt = head[u], head[u] = idx++;
    E[idx].to = u, E[idx].val = 0, E[idx].nxt = head[v], head[v] = idx++;
}

bool bfs() {
    queue<int> q;
    memset (dis, 0x3f, sizeof(dis));
    memcpy (cur, head, sizeof(head));
    q.emplace(S), dis[S] = 0;
    while (!q.empty()) {
        int u = q.front(); q.pop();
        for (int i = head[u]; ~i; i = E[i].nxt) {
            int v = E[i].to, w = E[i].val;
            if (w && dis[v] == inf) {
                dis[v] = dis[u] + 1;
                q.emplace(v);
            }
        }
    }
    return dis[T] != inf;
}

int dfs (int u, int flow) {
    if (u == T) 
        return flow;
    int sum = 0;
    for (int i = cur[u]; ~i; i = E[i].nxt) {
        int v = E[i].to, w = E[i].val; cur[u] = i;
        if (w && dis[v] == dis[u] + 1) {
            int tmpv = dfs (v, min(w, flow));
            E[i].val -= tmpv;
            E[i ^ 1].val += tmpv;
            sum += tmpv;
            flow -= tmpv;
            if (!flow) break;
        }
    }
    if (!sum)
        dis[u] = inf;
    return sum;
}

void dinic() {
    while (bfs())
        ans -= dfs(S, inf);
    for (int i = 1; i <= m; i ++) if (dis[i] != inf) cout << i << ' ';
    cout << '\n';
    for (int i = 1; i <= n; i ++) if (dis[m + i] != inf) cout << i << ' ';
    cout << '\n' << ans << '\n';
    return;
}

signed main() {

    scanf ("%lld %lld", &m, &n), T = n + m + 1;
    memset (head, -1, sizeof(head));
    for (int i = 1; i <= m; i ++) {
        int cost, x;
        scanf ("%lld", &cost);
        ans += cost;
        addedge (S, i, cost);
        cin.getline (buf, 500);
        int ulen = 0;
        while (sscanf (buf + ulen, "%lld", &x) == 1) {
            addedge (i, x + m, inf);
            if (!x)
                ulen++;
            while (x) {
                ulen++;
                x /= 10;
            }
            ulen++;
        }
    }
    for (int i = 1, w; i <= n; i ++) {
        scanf ("%lld", &w);
        addedge (m + i, T, w);
    }
    dinic();
    return 0;
}

Ex.3 P2764 最小路径覆盖问题

那么路径的终点出度一定为 0,称之为未匹配节点,那么我们最大流做一遍二分图最大匹配,相当于最小化了未匹配节点,那么最小路径覆盖等价于总点数减去最大匹配数。

方案的输出的话直接 dfs 出边对应在同一集合内的点,但是你发现这样会被最后链的点卡掉,此时我们需要并查集合并满足条件边的两端点然后 dfs 未匹配点。

#include <queue>
#include <cstring>
#include <iostream>
#include <algorithm>
#define int long long

using namespace std;
constexpr int N = 2e4 + 10;
constexpr int inf = 0x3f3f3f3f3f3f3f3f;

int n, m, idx, S, T;
int head[N], cur[N], dis[N], fa[N];
bool vst[N];

struct graph {
    int from;
    int to;
    int val;
    int nxt;
} E[N];

int find (int x) { 
    return fa[x] == x ? x : fa[x] = find(fa[x]); 
}

void merge (int x, int y) {
    x = find(x), y = find(y);
    if (x != y) fa[x] = y;
}

void addedge (int u, int v, int w) {
    E[idx].to = v, E[idx].val = w, E[idx].nxt = head[u], E[idx].from = u, head[u] = idx++;
    E[idx].to = u, E[idx].val = 0, E[idx].nxt = head[v], E[idx].from = v, head[v] = idx++;
}

bool bfs() {
    queue<int> q;
    memset (dis, 0x3f, sizeof(dis));
    memcpy (cur, head, sizeof(head));
    q.emplace(S), dis[S] = 0;
    while (!q.empty()) {
        int u = q.front(); q.pop();
        for (int i = head[u]; ~i; i = E[i].nxt) {
            int v = E[i].to, w = E[i].val;
            if (w && dis[v] == inf) {
                dis[v] = dis[u] + 1;
                q.emplace(v);
            }
        }
    }
    return dis[T] != inf;
}

int dfs (int u, int flow) {
    if (u == T)
        return flow;
    int sum = 0;
    for (int i = cur[u]; ~i; i = E[i].nxt) {
        int v = E[i].to, w = E[i].val; cur[u] = i;
        if (w && dis[v] == dis[u] + 1) {
            int tmpv = dfs (v, min(w, flow));
            E[i].val -= tmpv;
            E[i ^ 1].val += tmpv;
            sum += tmpv;
            flow -= tmpv;
            if (!flow) break;
        }
    }
    if (!sum)
        dis[u] = inf;
    return sum;
}

void print (int u) {
    if (vst[u])
        return;
    vst[u] = true;
    cout << u << ' ';
    for (int i = head[u]; ~i; i = E[i].nxt) {
        int v = E[i].to;
        if (v > n && v < T) {
            if (!E[i].val) 
                print (v - n);
        }
    }
}

void dinic() {
    int ans = 0;
    while (bfs())
        ans += dfs(S, inf);
    for (int i = 0; i < idx; i ++) {
        int u = E[i].from, v = E[i].to;
        if (u >= 1 && u <= n) {
            if (v > n && v < T) {
                if (!E[i].val) 
                    merge (v - n, u);
            }
        }
    }
    for (int i = 1; i <= n; i ++) {
        if (fa[i] == i) {
            print(i);
            cout << '\n';
        }
    }
    cout << n - ans << '\n';
}

signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);

    cin >> n >> m;
    T = 2 * n + 1;
    memset (head, -1, sizeof(head));
    for (int i = 1; i <= m; i ++) {
        int u, v; 
        cin >> u >> v;
        addedge (u, v + n, 1);
    }
    for (int i = 1; i <= n; i ++) 
        addedge (S, i, 1), addedge (n + i, T, 1);
    for (int i = 1; i < T; i ++) fa[i] = i;
    dinic();
    return 0;
}

最小割

记原图边集为 E,最小割指对于包含 S,T 的网络,存在边集 E' \in E,使得删除 E' 中的边后 S,T 不连通且容量之和最小。

引:最大流最小割定理

指网络的最大流的值等于最小割。

意会一下就是要使选出边集的容量之和最小,那么每条增广路一定是选容量最小的那条边,与最大流原理相同。

Ex. P1344 [USACO4.4] 追查坏牛奶 Pollutant Control

最小割模板。

但是要求割边数量,此时我们发现 \sum\limits_{i=1}^{m}{w_i} = ans,变形一下得到 \sum\limits_{i=1}^{m}{kw_i + 1} = k \times ans + m,那么最后所求得的 \frac{ans'}{k} 就是最小割,ans' \bmod k 就是割边数。

#include <queue>
#include <cstring>
#include <iostream>
#include <algorithm>
#define int long long

using namespace std;
constexpr int base = 2025;
constexpr int N = 2e3 + 10;
constexpr int inf = 0x3f3f3f3f3f3f3f3f;

int head[N], cur[N], dis[N];
int n, m, idx, anst, ansf, S, T;

struct graph {
    int to;
    int nxt;
    int val;
} E[N];

void addedge (int u, int v, int w) {
    E[idx].to = v;
    E[idx].val = w;
    E[idx].nxt = head[u];
    head[u] = idx++;
}

bool bfs() {
    queue<int> q;
    memcpy (cur, head, sizeof(head));
    memset (dis, 0x3f, sizeof(dis));
    q.emplace(S), dis[S] = 0;
    while (!q.empty()) {
        int now = q.front(); q.pop();
        for (int i = head[now]; ~i; i = E[i].nxt) {
            int v = E[i].to, w = E[i].val;
            if (w && dis[v] == inf) {
                dis[v] = dis[now] + 1;
                q.emplace(v);
            }
        }
    }
    return dis[T] != inf;
}

int dfs (int now, int flow) {
    if (now == T)
        return flow;
    int sum = 0;
    for (int i = cur[now]; ~i; i = E[i].nxt) {
        int v = E[i].to, w = E[i].val;
        cur[now] = i;
        if (w && dis[v] == dis[now] + 1) {
            int tmpv = dfs (v, min(w, flow));
            E[i].val -= tmpv;
            E[i ^ 1].val += tmpv;
            sum += tmpv;
            flow -= tmpv;
            if (!flow) break;
        }
    }
    if (!sum)
        dis[now] = inf;
    return sum;
}

void dinic() {
    while (bfs()) {
        int tmpv = dfs(S, inf);
        ansf += tmpv / base;
        anst += tmpv % base;
    }
    return;
}

signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);

    cin >> n >> m;
    S = 1, T = n;
    memset (head, -1, sizeof(head));
    for (int i = 1; i <= m; i ++) {
        int u, v, w;
        cin >> u >> v >> w;
        addedge (u, v, w * base + 1);
        addedge (v, u, 0);
    }
    dinic();
    cout << ansf << ' ' << anst << '\n';
    return 0;
}

费用流

顾名思义,费用流指在边上除容量外另增加一个权值为费用表示流过这条边的单位流量所需支付的费用,其实不难发现,Dinic 算法的本质是按照层数从小到大进行 dfs,费用就是相当于把层数换成最短路径长度,把 bfs 换成 spfa 就行,如果要求最大流时的费用,那么就是最大流乘上到 T 的最短路径长度(每条边流过最大流值的流量)。

当然费用流分为最小费用最大流和最大费用最大流(分别对应最短路和最长路)。

Ex.1 P3381 【模板】最小费用最大流

#include <queue>
#include <cstring>
#include <iostream>
#include <algorithm>
#define int long long

using namespace std;
constexpr int N = 1e5 + 10;
constexpr int inf = 0x3f3f3f3f3f3f3f3f;

int n, m, S, T, idx, ansc, ansf;
int head[N], cur[N], dis[N];
bool vst[N], pass[N];

struct graph {
    int to;
    int nxt;
    int val;
    int cost;
} E[N];

void addedge (int u, int v, int w, int c) {
    E[idx].to = v, E[idx].val = w, E[idx].nxt = head[u], E[idx].cost = c, head[u] = idx++;
}

bool spfa() {
    queue<int> q;
    memset (dis, 0x3f, sizeof(dis));
    memset (vst, false, sizeof(vst));
    q.emplace(S);
    dis[S] = 0, vst[S] = true;
    while (!q.empty()) {
        int now = q.front(); q.pop();
        vst[now] = false;
        for (int i = head[now]; ~i; i = E[i].nxt) {
            int to = E[i].to, val = E[i].val, cost = E[i].cost;
            if (val && dis[to] > dis[now] + cost) {
                dis[to] = dis[now] + cost;
                if (!vst[to]) {
                    vst[to] = true;
                    q.emplace(to);
                }
            }
        }
    }
    return dis[T] != inf;
}

int dfs (int now, int flow) {
    if (now == T) 
        return flow;
    pass[now] = true; // 只遍历没有访问过的点
    int sum = 0;
    for (int i = cur[now]; ~i; i = E[i].nxt) {
        cur[now] = i;
        int to = E[i].to, val = E[i].val, cost = E[i].cost;
        if (!pass[to] && val && dis[to] == dis[now] + cost) {
            int tmpv = dfs (to, min(val, flow));
            E[i].val -= tmpv, E[i ^ 1].val += tmpv;
            sum += tmpv, flow -= tmpv;
            if (!flow) break;
        }
    }
    pass[now] = false;
    if (!sum)
        dis[now] = inf;
    return sum;
}

void dinic() {
    while (spfa()) {
        memcpy (cur, head, sizeof(head));
        int res = dfs (S, inf);
        ansf += res, ansc += res * dis[T];
    }
}

signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    cin >> n >> m >> S >> T;
    memset (head, -1, sizeof(head));
    for (int i = 1; i <= m; i ++) {
        int u, v, w, c;
        cin >> u >> v >> w >> c;
        addedge (u, v, w, c), addedge (v, u, 0, -c); // 反边花费为相反数
    }
    dinic();
    cout << ansf << ' ' << ansc << '\n';
    return 0;
}

Ex.2 P4014 分配问题

工作和人拉出来成二分图,然后连边,工作和人之间连费用为 c_{i,j} 流量 1 的边,然后超级源点连两集合中一个花费 0 流量 1,另一集合连超级汇点花费 0 流量 1,然后分别是最小费用最大流、最大费用最大流。

#include <queue>
#include <cstring>
#include <iostream>
#include <algorithm>
#define int long long

using namespace std;
constexpr int N = 1e4 + 10;
constexpr int inf = 0x3f3f3f3f3f3f3f3f;

int n, idx, S, T;
int head[N], cur[N], dis[N];
bool vst[N], pass[N];

struct graph {
    int to;
    int val;
    int nxt;
    int cost;
} E1[N << 1], E2[N << 1];

void addedge (int u, int v, int w, int c) {
    E1[idx].to = v, E1[idx].val = w, E1[idx].cost = c, E1[idx].nxt = head[u], head[u] = idx++;
    E1[idx].to = u, E1[idx].val = 0, E1[idx].cost = -c, E1[idx].nxt = head[v], head[v] = idx++;
} 

bool spfa (bool opt) {
    queue<int> q;
    memset (vst, false, sizeof(vst));
    memcpy (cur, head, sizeof(head));
    memset (dis, opt ? 0x3f : ~0x3f, sizeof(dis));
    q.emplace(S), vst[S] = true, dis[S] = 0;
    while (!q.empty()) {
        int u = q.front(); q.pop();
        vst[u] = false;
        for (int i = head[u]; ~i; i = E2[i].nxt) {
            int v = E2[i].to, w = E2[i].val, c = E2[i].cost;
            if (opt) {
                if (dis[v] > dis[u] + c && w) {
                    dis[v] = dis[u] + c;
                    if (!vst[v]) {
                        vst[v] = true;
                        q.emplace(v);
                    }
                }
            } else {
                if (dis[v] < dis[u] + c && w) {
                    dis[v] = dis[u] + c;
                    if (!vst[v]) {
                        vst[v] = true;
                        q.emplace(v);
                    }
                }
            }
        }
    }
    return opt ? dis[T] != inf : dis[T] != ~inf;
}

int dfs (int u, int flow, bool opt) {
    if (u == T) return flow;
    int sum = 0;
    pass[u] = true;
    for (int i = cur[u]; ~i; i = E2[i].nxt) {
        int v = E2[i].to, w = E2[i].val, c = E2[i].cost;
        cur[u] = i;
        if (dis[v] == dis[u] + c && w && !pass[v]) {
            int tmpv = dfs (v, min (w, flow), opt);
            E2[i].val -= tmpv;
            E2[i ^ 1].val += tmpv;
            sum += tmpv;
            flow -= tmpv;
            if (!flow) break;
        }
    }
    pass[u] = false;
    if (!sum) dis[u] = opt ? inf : -inf;
    return sum;
}

void dinic (bool opt) {
    int ans = 0;
    while (spfa(opt))
        ans += dfs (S, inf, opt) * dis[T];
    cout << ans << '\n'; 
}

signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);

    cin >> n;
    T = n << 1 | 1;
    memset (head, -1, sizeof(head));
    for (int i = 1; i <= n; i ++) {
        for (int j = 1; j <= n; j ++) {
            int x; cin >> x;
            addedge (j, n + i, 1, x);
        }
    }
    for (int i = 1; i <= n; i ++)
        addedge (S, i, 1, 0);
    for (int i = n + 1; i < T; i ++)
        addedge (i, T, 1, 0);
    memcpy (E2, E1, sizeof(E1)), dinic(true);
    memcpy (E2, E1, sizeof(E1)), dinic(false);
    return 0;
}

结语

网络流其实最难的就是如何建模(建图),建出图来就都好说了,也有可能和分层图网络流。

习题

网络流 24 题。

list No.1。

list No.2。