网络流

\(\text{luogu-3376}\)

给出一个 \(n\) 个点 \(m\) 条边的有向网格图,以及其源点 \(s\) 汇点 \(t\) 和每条边的流量 \(w_i\),求出其网络最大流。

\(1 \le n \le 200\)\(1 \le m \le 5000\)\(0 \le w_i \le 2^{31}\)


Dinic 模板题。

算法详细见 Dinic算法(研究总结,网络流) - SYCstudio - 博客园

这里说说主要的。

为了处理朴素增广的不正确时间复杂度,Dinic 会在每次增广前给每个点分配一个深度 \(dep_i\)

然后我们进行若干遍 dfs 寻找增广路,每次 \(u \to v\) 必须保证 \(dep_v = dep_u + 1\)

于是 Dinic 的算法流程大概是这样:

  • bfs 分配每个点的深度 \(dep_i\)
    • 若干遍 dfs 寻找增广路。
    • 将增广路的贡献加入答案,若没有增广路则跳出。
  • 回到第一步。

进行若干遍增广之后就会找到最大流。

时间复杂度的上界是 \(O(n^2m)\),但大概率跑不满。

注意:链式前向星存图边的编号要从 \(0/2\) 开始。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
using namespace std;
#define ll long long
#define MAXN 5005
#define INF 0x3f3f3f3f

ll read() {
    ll x = 0, f = 1;
    char c = getchar();
    while(c < 48 || c > 57) { if(c == 45) f = -1; c = getchar(); }
    while(c >= 48 && c <= 57) { x = (x << 3) + (x << 1) + (c - 48); c = getchar(); }
    return x * f;
}

ll n, m, s, t, hd[MAXN], tot = -1, nxt[MAXN], v[MAXN], w[MAXN], res;
ll dep[MAXN], cur[MAXN];

void add(ll x, ll y, ll W) {
    nxt[++ tot] = hd[x], v[tot] = y;
    w[tot] = W, hd[x] = tot; return;
}

bool bfs() {
    memset(dep, 0, sizeof dep);
    queue<ll> q; q.push(s), dep[s] = 1;
    while(!q.empty()) {
        ll x = q.front(); q.pop();
        for(int i = hd[x]; i != -1; i = nxt[i]) 
            if(!dep[v[i]] && w[i] > 0) dep[v[i]] = dep[x] + 1, q.push(v[i]);
    }
    if(dep[t] > 0) return 1;
    return 0;
}

ll dfs(ll x, ll flow) {
    if(x == t) return flow;
    for(ll &i = cur[x]; i != -1; i = nxt[i])
        if(dep[v[i]] == dep[x] + 1 && w[i] != 0) {
            ll d = dfs(v[i], min(flow, w[i]));
            if(d > 0) {
                w[i] -= d, w[i ^ 1] += d;
                return d;
            }
        }
    return 0;
}

int main() {
    n = read(), m = read(), s = read(), t = read();
    memset(hd, -1, sizeof hd), memset(nxt, -1, sizeof nxt);
    for(int i = 1; i <= m; i ++) {
        ll x = read(), y = read(), W = read();
        add(x, y, W), add(y, x, 0);
    }
    while(bfs()) {
        for(int i = 1; i <= n; i ++) cur[i] = hd[i];
        while(ll d = dfs(s, INF)) res += d;
    }
    cout << res << "\n";
    return 0;
}

\(\text{loj-6000}\)

飞行大队有 \(n\) 个来自各地的驾驶员,专门驾驶一种型号的飞机,这种飞机每架有两个驾驶员,需一个正驾驶员和一个副驾驶员。由于种种原因,例如相互配合的问题,有些驾驶员不能在同一架飞机上飞行,问如何搭配驾驶员才能使出航的飞机最多。

因为驾驶工作分工严格,两个正驾驶员或两个副驾驶员都不能同机飞行。编号前 \(m\) 个都是正驾驶员。

\(2 \le n \le 100\)


二分图最大匹配问题。

实际上可转化为网络流,考虑建一个虚拟源点 \(s = 0\) 和一个虚拟汇点 \(t = n + 1\)

我们让 \(s\) 与正驾驶员相连,建单向边 \(s \to i,i \in[1, m]\),副驾驶员与 \(t\) 相连,建单向边 \(i \to t, i \in [m + 1, n]\)

每条边的最大流量都是 \(1\)。然后把题中给的正驾驶员和副驾驶员建单向边,最大流量设为 \(\infty\)

然后跑 \(s \to t\) 的网络流就好了,最大流即为答案。

Dinic 在二分图上跑的时候时间复杂度优化为 \(O(nm)\)

注意:链式前向星一定要初始化 \(hd_i\)\(nxt_i\) 啊。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
using namespace std;
#define ll long long
#define MAXN 10005
#define INF 0x3f3f3f3f

ll read() {
    ll x = 0, f = 1;
    char c = getchar();
    while(c < 48 || c > 57) { if(c == 45) f = -1; c = getchar(); }
    while(c >= 48 && c <= 57) { x = (x << 3) + (x << 1) + (c - 48); c = getchar(); }
    return x * f;
}

ll n, m, s, t, hd[MAXN], tot = -1, nxt[MAXN], v[MAXN], w[MAXN], res;
ll dep[MAXN], cur[MAXN];

void add(ll x, ll y, ll W) {
    nxt[++ tot] = hd[x], v[tot] = y;
    w[tot] = W, hd[x] = tot; return;
}

bool bfs() {
    memset(dep, 0, sizeof dep);
    queue<ll> q; q.push(s), dep[s] = 1;
    while(!q.empty()) {
        ll x = q.front(); q.pop();
        for(int i = hd[x]; i != -1; i = nxt[i]) 
            if(!dep[v[i]] && w[i] > 0) dep[v[i]] = dep[x] + 1, q.push(v[i]);
    }
    if(dep[t] > 0) return 1;
    return 0;
}

ll dfs(ll x, ll flow) {
    if(x == t) return flow;
    for(ll &i = cur[x]; i != -1; i = nxt[i])
        if(dep[v[i]] == dep[x] + 1 && w[i] != 0) {
            ll d = dfs(v[i], min(flow, w[i]));
            if(d > 0) {
                w[i] -= d, w[i ^ 1] += d;
                return d;
            }
        }
    return 0;
}

int main() {
    n = read(), m = read(), s = read(), t = read();
    memset(hd, -1, sizeof hd), memset(nxt, -1, sizeof nxt);
    for(int i = 1; i <= m; i ++) {
        ll x = read(), y = read(), W = read();
        add(x, y, W), add(y, x, 0);
    }
    while(bfs()) {
        for(int i = 1; i <= n; i ++) cur[i] = hd[i];
        while(ll d = dfs(s, INF)) res += d;
    }
    cout << res << "\n";
    return 0;
}

\(\text{luogu-3381}\)

给出一个包含 \(n\) 个点和 \(m\) 条边的有向图(下面称其为网络) \(G=(V,E)\),该网络上所有点分别编号为 \(1 \sim n\),所有边分别编号为 \(1\sim m\),其中该网络的源点为 \(s\),汇点为 \(t\),网络上的每条边 \((u,v)\) 都有一个流量限制 \(w(u,v)\) 和单位流量的费用 \(c(u,v)\)

你需要给每条边 \((u,v)\) 确定一个流量 \(f(u,v)\),要求:

  1. \(0 \leq f(u,v) \leq w(u,v)\)(每条边的流量不超过其流量限制);
  2. \(\forall p \in \{V \setminus \{s,t\}\}\)\(\sum_{(i,p) \in E}f(i,p)=\sum_{(p,i)\in E}f(p,i)\)(除了源点和汇点外,其他各点流入的流量和流出的流量相等);
  3. \(\sum_{(s,i)\in E}f(s,i)=\sum_{(i,t)\in E}f(i,t)\)(源点流出的流量等于汇点流入的流量)。

定义网络 \(G\) 的流量 \(F(G)=\sum_{(s,i)\in E}f(s,i)\),网络 \(G\) 的费用 \(C(G)=\sum_{(i,j)\in E} f(i,j) \times c(i,j)\)

你需要求出该网络的最小费用最大流,即在 \(F(G)\) 最大的前提下,使 \(C(G)\) 最小。

\(1 \leq n \leq 5\times 10^3\)\(1 \leq m \leq 5 \times 10^4\)\(1 \leq s,t \leq n\)\(u_i \neq v_i\)\(0 \leq w_i,c_i \leq 10^3\)


最小费用最大流模板题。

实际上和 Dinic 的写法差不多,最小费用最大流是贪心地每次按费用最短路进行增广。

因为有负权边,直接把 bfs 换成 spfa 就好,每次找到 \(s \to t\) 的费用最短路增广,可以证明这样得到的答案最优。

注意:dfs 过程中可能遇到负环,要跳过负环,不然会死循环。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
using namespace std;
#define ll long long
#define MAXN 100005
#define INF 0x3f3f3f3f

ll read() {
    ll x = 0, f = 1;
    char c = getchar();
    while(c < 48 || c > 57) { if(c == 45) f = -1; c = getchar(); }
    while(c >= 48 && c <= 57) { x = (x << 3) + (x << 1) + (c - 48); c = getchar(); }
    return x * f;
}

ll n, m, s, t, hd[MAXN], tot = 1, nxt[MAXN], to[MAXN], w[MAXN], c[MAXN];
ll cur[MAXN], dis[MAXN], res1, res2;
bool vis[MAXN];

void add(ll x, ll y, ll W, ll C) {
    nxt[++ tot] = hd[x], to[tot] = y;
    w[tot] = W, c[tot] = C, hd[x] = tot;
    return;
}

bool spfa() {
    memset(vis, 0, sizeof vis);
    for(int i = 1; i <= n; i ++) dis[i] = INF;
    queue<ll> q; q.push(s), dis[s] = 0, vis[s] = 1;
    while(!q.empty()) {
        ll x = q.front(); q.pop();
        vis[x] = 0;
        for(int i = hd[x]; i != -1; i = nxt[i]) {
            ll y = to[i];
            if(w[i] && dis[y] > dis[x] + c[i]) {
                dis[y] = dis[x] + c[i];
                if(!vis[y]) vis[y] = 1, q.push(y);
            }
        }
    }
    if(dis[t] < INF) return 1;
    return 0;
}

ll dfs(ll x, ll flow) {
    if(x == t) return flow; vis[x] = 1;
    for(ll &i = cur[x]; i != -1; i = nxt[i])
        if(w[i] != 0 && dis[to[i]] == dis[x] + c[i] && !vis[to[i]]) {
            ll d = dfs(to[i], min(flow, w[i]));
            if(d > 0) {
                w[i] -= d, w[i ^ 1] += d;
                res2 += d * c[i], vis[x] = 0; 
                return d;
            }
        }
    vis[x] = 0;
    return 0;
}

int main() {
    n = read(), m = read(), s = read(), t = read();
    memset(hd, -1, sizeof hd), memset(nxt, -1, sizeof nxt);
    for(int i = 1; i <= m; i ++) {
        ll x = read(), y = read(), w = read(), c = read();
        add(x, y, w, c), add(y, x, 0, -c);
    }
    while(spfa()) {
        for(int i = 1; i <= n; i ++) cur[i] = hd[i];
        while(ll d = dfs(s, INF)) res1 += d;
    }
    cout << res1 << " " << res2 << "\n";
    return 0;
}

\(\text{loj-6001} / \text{luogu-2762}\)

W 教授正在为国家航天中心计划一系列的太空飞行。每次太空飞行可进行一系列商业性实验而获取利润。现已确定了一个可供选择的实验集合 $ E = { E_1, E_2, \cdots, E_m } $,和进行这些实验需要使用的全部仪器的集合 $ I = { I_1, I_2, \cdots, I_n } $。实验 $ E_j $ 需要用到的仪器是 $ I $ 的子集 $ R_j \subseteq I $。

配置仪器 $ I_k $ 的费用为 $ c_k $ 美元。实验 $ E_j $ 的赞助商已同意为该实验结果支付 $ p_j $ 美元。W 教授的任务是找出一个有效算法,确定在一次太空飞行中要进行哪些实验并因此而配置哪些仪器才能使太空飞行的净收益最大。这里净收益是指进行实验所获得的全部收入与配置仪器的全部费用的差额。

对于给定的实验和仪器配置情况,编程找出净收益最大的试验计划。

$1 \leq n, m \leq 50 ,1 \leq c,p < 2^{31} $。


最大权闭合子图模板题。

考虑建图跑网络流,首先肯定要从实验到仪器建单向边,流量为 \(\infty\)

那么,考虑建一个虚拟源点 \(s=0\),向所有实验 \(i, i\in [n+1,n+m]\) 建单向边,流量为 \(p_i\)

建一个虚拟汇点 \(t = n + m + 1\),所有仪器 \(i, i \in [1,n]\) 向汇点建单向边,流量为 \(c_i\)

此时从源点跑最小割,如果割掉了 \(s\) 向实验的连边,则说明不选择该实验;如果割掉了仪器向 \(t\) 的连边,则说明选择该仪器;显然不会割掉实验到仪器的连边,因为流量为 \(\infty\)

此时最大权就是所有正点权减去最小割,也就是 \(\sum p_i - w\),其中 \(w\) 为最小割。

最大权闭合子图的方案可以感性理解,最后一次 Dinic 跑完之后,被分到层深度的点就是被选择的点。

下面是一种比较详细的解释,来自于 huangzirui - 个人中心 - 洛谷

这里大概讲一下转换成最大流以后怎么输出。

一个结论就是假如我们跑的是 Dinic 那么我们最后一次网络流(这一次网络流并没有起任何作用,只是确认了无更多残余流量可以退出了。)中所有被分到层的都一定被选上了。

没有更多残余流量其实意味着这个图已经被割成了两部分,一个实验如果有层数意味着它没有被割掉(被选上了),一个仪器如果有层数意味着它已经被割掉了(也是被选上了)。

于是只要在最后输出所有有层数的点就行了。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
using namespace std;
#define ll long long
#define MAXN 10005
#define INF 0x3f3f3f3f

ll read() {
    ll x = 0, f = 1;
    char c = getchar();
    while(c < 48 || c > 57) { if(c == 45) f = -1; c = getchar(); }
    while(c >= 48 && c <= 57) { x = (x << 3) + (x << 1) + (c - 48); c = getchar(); }
    return x * f;
}

ll n, m, s, t, hd[MAXN], tot = -1, nxt[MAXN], to[MAXN], w[MAXN], sum;
ll cur[MAXN], dep[MAXN], res;

void add(ll x, ll y, ll W) {
    nxt[++ tot] = hd[x], to[tot] = y;
    w[tot] = W, hd[x] = tot; return;
}

bool bfs() {
    memset(dep, 0, sizeof dep);
    queue<ll> q; q.push(s), dep[s] = 1;
    while(!q.empty()) {
        ll x = q.front(); q.pop();
        for(int i = hd[x]; i != -1; i = nxt[i])
            if(!dep[to[i]] && w[i]) dep[to[i]] = dep[x] + 1, q.push(to[i]);
    }
    if(dep[t] > 0) return 1;
    return 0;
}

ll dfs(ll x, ll flow) {
    if(x == t) return flow;
    for(ll &i = cur[x]; i != -1; i = nxt[i])
        if(dep[to[i]] == dep[x] + 1 && w[i]) {
            ll d = dfs(to[i], min(flow, w[i]));
            if(d > 0) {
                w[i] -= d, w[i ^ 1] += d;
                return d;
            }
        }
    return 0;
}

int main() {
    cin >> m >> n; s = 0, t = n + m + 1;
    memset(hd, -1, sizeof hd), memset(nxt, -1, sizeof nxt);
    for(int i = 1; i <= m; i ++) {
        ll x; cin >> x; sum += x;
        add(s, i + n, x), add(i + n, s, 0);
        while(cin.get() == ' ') {
            cin >> x;
            add(i + n, x, INF);
            add(x, i + n, 0);
        }
    }
    for(int i = 1; i <= n; i ++) {
        ll x; cin >> x;
        add(i, t, x), add(t, i, 0);
    }
    while(bfs()) {
        for(int i = 0; i <= n + m + 1; i ++) cur[i] = hd[i];
        while(ll d = dfs(s, INF)) res += d;
    }
    for(int i = 1; i <= m; i ++) if(dep[i + n]) cout << i << " ";
    cout << "\n";
    for(int i = 1; i <= n; i ++) if(dep[i]) cout << i << " ";
    cout << "\n" << sum - res << "\n";
    return 0;
}

\(\text{loj-6012} / \text{luogu-4014}\)

\(n\) 件工作要分配给 \(n\) 个人做。第 \(i\) 个人做第 \(j\) 件工作产生的效益为 \(c_{ij}\) 。试设计一个将 \(n\) 件工作分配给 \(n\) 个人做的分配方案,使产生的总效益最小或最大。一个人只能修一个工件。

\(1 \leq n \leq 50, 0 \le c _ {i, j} \le 100\)


前天模拟赛 \(A\) 题跟这个很像,但做法不太一样,赛时因为思路有点偏,当时把问题转化成跟这个一摸一样的了。

回到这道题,考虑拆点,把每个点 \(i\) 拆成入点 \(i\) 和出点 \(i+n\),考虑如下建图:

  • 所有虚拟源点 \(s=0\) 与所有入点连边,流量为 \(1\),费用为 \(0\)
  • 所有出点与虚拟汇点 \(t = 2n+1\) 连边,流量为 \(1\),费用为 \(0\)
  • 对于所有 \(i,j\),建单向边 \(i \to j+n\),流量为 \(1\),费用为 \(c_{i,j}\)

此时跑最小费用最大流,最小费用对应的就是总效益最小值,最大流此时显然为 \(n\),可以不用管。

最大值也很好求,把第三种边的费用改为 \(-c_{i,j}\),跑最小费用最大流,此时最小费用的相反数就是总效益最大值。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
using namespace std;
#define ll long long
#define MAXN 100005
#define MAXM 505
#define INF 0x3f3f3f3f

ll read() {
    ll x = 0, f = 1;
    char c = getchar();
    while(c < 48 || c > 57) { if(c == 45) f = -1; c = getchar(); }
    while(c >= 48 && c <= 57) { x = (x << 3) + (x << 1) + (c - 48); c = getchar(); }
    return x * f;
}

ll n, s, t, a[MAXM][MAXM], hd[MAXN], tot = 1, nxt[MAXN], to[MAXN], w[MAXN], c[MAXN];
ll cur[MAXN], dis[MAXN], res;
bool vis[MAXN];

void add(ll x, ll y, ll W, ll C) {
    nxt[++ tot] = hd[x], to[tot] = y, w[tot] = W, c[tot] = C, hd[x] = tot;
    nxt[++ tot] = hd[y], to[tot] = x, w[tot] = 0, c[tot] = -C, hd[y] = tot;
    return;
}

bool spfa() {
    memset(vis, 0, sizeof vis);
    memset(dis, INF, sizeof dis);
    queue<ll> q; q.push(s), dis[s] = 0, vis[s] = 1;
    while(!q.empty()) {
        ll x = q.front(); q.pop();
        vis[x] = 0;
        for(int i = hd[x]; i != -1; i = nxt[i]) {
            ll y = to[i];
            if(w[i] && dis[y] > dis[x] + c[i]) {
                dis[y] = dis[x] + c[i];
                if(!vis[y]) vis[y] = 1, q.push(y);
            }
        }
    }
    if(dis[t] < INF) return 1;
    return 0;
}

ll dfs(ll x, ll flow) {
    if(x == t) return flow; vis[x] = 1;
    for(ll &i = cur[x]; i != -1; i = nxt[i])
        if(w[i] != 0 && dis[to[i]] == dis[x] + c[i] && !vis[to[i]]) {
            ll d = dfs(to[i], min(flow, w[i]));
            if(d > 0) {
                w[i] -= d, w[i ^ 1] += d;
                res += d * c[i], vis[x] = 0; 
                return d;
            }
        }
    vis[x] = 0;
    return 0;
}

int main() {
    n = read(), s = 0, t = 2 * n + 1;
    for(int i = 1; i <= n; i ++) for(int j = 1; j <= n; j ++) a[i][j] = read();
    memset(hd, -1, sizeof hd), memset(nxt, -1, sizeof nxt);
    for(int i = 1; i <= n; i ++) add(s, i, 1, 0), add(i + n, t, 1, 0);
    for(int i = 1; i <= n; i ++) for(int j = 1; j <= n; j ++) add(i, j + n, 1, a[i][j]);
    while(spfa()) {
        for(int i = 0; i <= 2 * n + 1; i ++) cur[i] = hd[i];
        while(dfs(s, INF));
    }
    cout << res << "\n"; res = 0, tot = -1;
    memset(hd, -1, sizeof hd), memset(nxt, -1, sizeof nxt);
    for(int i = 1; i <= n; i ++) add(s, i, 1, 0), add(i + n, t, 1, 0);
    for(int i = 1; i <= n; i ++) for(int j = 1; j <= n; j ++) add(i, j + n, 1, -a[i][j]);
    while(spfa()) {
        for(int i = 0; i <= 2 * n + 1; i ++) cur[i] = hd[i];
        while(dfs(s, INF));
    }
    cout << -res << "\n";
    return 0;
}
posted @ 2026-03-05 19:26  So_noSlack  阅读(7)  评论(0)    收藏  举报