网络流学习笔记

· · 算法·理论

本文使用老师同学以及人工智能进行润色,保证我的贡献严格大于他们的贡献。

一、一些概念

1.1 定义

一个流网络 G=(V,E) 是一个有向图,满足以下条件:

1.2 注意事项

二、流

2.1 定义

流网络 G=(V,E) 上的一个流是一个从边集到非负实数的映射,它必须满足以下两个条件:

  1. 对每条边 e \in E,都有:
0 \le f_e \le c_e

即每条边上的流量不得超过其容量。流量不能为负,表示物质不能逆流。

  1. 对每个中间顶点 u \in V \set \min us \{s,t\},都有:
\sum_{e \in \text{in}(u)} f_e = \sum_{e \in \text{out}(u)} f_e

其中 \text{in}(u) 表示指向 u 的边集,\text{out}(u) 表示从 u 出发的边集。

说人话就是流入该点的总流量等于流出该点的总流量。也称为能量守恒。

2.2 流值

流的流值定义为从源点流出的总流量:

|f| = \sum_{e \in \text{out}(s)} f_e - \sum_{e \in \text{in}(s)} f_e

如果源点没有入边(或约定不计入),则简化为:

|f| = \sum_{e \in \text{out}(s)} f_e

根据流量守恒,流值也等于流入汇点的总流量:

|f| = \sum_{e \in \text{in}(t)} f_e

可以把图化为水管:

2.3 零流与满流

三、最大流问题

3.1 问题定义

我理解这个定义花了一个晚上,最简单最直接最不绕弯子的定义表述:源点有 x 的水量,通过多次水管运输且单个水管运输量不超过水管容量的情况下能将 x 的水全部运输到汇点的最大的满足要求的 x

四、残量网络

4.1 残量

对于流网络 G 上的流 f,定义每条边 e=(u,v) 的残量为:

c'_e = c_e - f_e

残量表示该边还能通过多少流量。

4.2 残量网络

残量网络 G_f 是一个基于原网络和当前流构造的新网络:

4.3 反向边的意义

反向边是网络流中一个非常巧妙的设计:

想象一条管道 u \to v 已经流了 5 单位流量。反向边 v \to u 的容量为 5,表示 " 可以从 v 端抽回 5 单位流量 ",这相当于让 u \to v 少流 5

4.4 残量网络的性质

五、增广路与增广

5.1 增广路

增广路是残量网络 G_f 中从源点 s 到汇点 t 的一条简单路径,路径上每条边的残量都大于 0

5.2 增广

增广是沿增广路推送流量的过程:

  1. 找到增广路 P,计算路径上最小残量:
\Delta = \min_{e \in P} c'_e
  1. 对路径上每条边:

    • 若是正向边 e=(u,v)f_e \gets f_e + \Delta(增加流量)
    • 若是反向边 e=(v,u)f_{(u,v)} \gets f_{(u,v)} - \Delta(撤销流量)
  2. 流值增加 \Delta|f| \gets |f| + \Delta

5.3 增广路定理

增广路定理:

一个流 f 是最大流当且仅当其残量网络 G_f 中不存在从 st 的增广路。

这给出了求最大流的基本思路:不断找增广路并增广,直到找不到为止。

六、最大流最小割

6.1 割

流网络 G=(V,E) 的一个割是将顶点集 V 分为两部分 ST=V \set \min us S,满足 s \in St \in T

6.2 割的容量

(S,T) 的容量定义为从 S 指向 T 的所有边的容量之和:

c(S,T) = \sum_{e \in \text{out}(S), \text{head}(e) \in T} c_e

注意:只计算从 ST 的边,不计算从 TS 的边。

6.3 割的流量

(S,T) 的流量定义为:

f(S,T) = \sum_{e \in \text{out}(S), \text{head}(e) \in T} f_e - \sum_{e \in \text{in}(S), \text{tail}(e) \in T} f_e

即从 S 流向 T 的总流量减去从 T 流向 S 的总流量。

6.4 流值与割的关系

对任意割 (S,T),都有:

|f| = f(S,T)

即流值等于任意割的净流量。这是由流量守恒推导得到的。

6.5 割的容量下界

对任意流 f 和任意割 (S,T),都有:

|f| \le c(S,T)

即流值不超过任意割的容量。这给出了最大流的一个上界。

6.6 最小割

最小割问题是求所有割中容量最小的割:

\min_{(S,T)} c(S,T)

6.7 最大流最小割定理

最大流最小割定理:

\max |f| = \min c(S,T)

即最大流等于最小割。

算法

EK 算法

这个算法比较简单,每次找到一条增广路(第 5 章会讲是什么)然后将这条增广路加上增广路的最小边剩余容量,一直找直到找不到为止,答案就是找到的所有增广路的最小边剩余容量之和。

代码:

#include <bits/stdc++.h>
using namespace std;
#define int long long
int n, m, s, t;
vector<pair<pair<int, int>, int>> vec[10005];
int vis[10005];
struct edge
{
    int v1, w1, id;
} pre[10005];
bool bfs()
{
    queue<int> que;
    memset(vis, 0, sizeof(vis));
    vis[s] = 1;
    que.push(s);
    while (!que.empty())
    {
        int v = que.front();
        que.pop();
        if (v == t)
            return 1;
        for (int i = 1; i < vec[v].size(); i++)
        {
            if (vec[v][i].first.second && !vis[vec[v][i].first.first])
            {
                vis[vec[v][i].first.first] = 1;
                pre[vec[v][i].first.first].v1 = v;
                pre[vec[v][i].first.first].w1 = vec[v][i].first.second;
                pre[vec[v][i].first.first].id = i;
                que.push(vec[v][i].first.first);
            }
        }
    }
    return 0;
}
signed main()
{
    cin >> n >> m >> s >> t;
    for (int i = 1; i <= n; i++)
    {
        vec[i].push_back({{0, 0}, 0});
    }
    for (int i = 1; i <= m; i++)
    {
        int u, v, w;
        cin >> u >> v >> w;
        vec[u].push_back({{v, w}, vec[v].size()});
        vec[v].push_back({{u, 0}, vec[u].size() - 1});
    }
    int ans = 0;
    while (bfs())
    {
        int minn = 1e18;
        for (int i = t; i != s; i = pre[i].v1)
        {
            minn = min(minn, pre[i].w1);
        }
        for (int i = t; i != s; i = pre[i].v1)
        {
            vec[pre[i].v1][pre[i].id].first.second -= minn;
            vec[i][vec[pre[i].v1][pre[i].id].second].first.second += minn;
        }
        ans += minn;
    }
    cout << ans << endl;
}

复杂度:O(nm^2),但是远达不到。

模板题是 P3376。

dinic 算法

EK 算法还是太不牛了,一次只能找一个增广路,有没有什么算法一次找多个?

有的兄弟有的,Dinic 算法。

Dinic 算法利用了分层图。

在残量网络中,从源点 s 做 BFS,记录每个点 us 的最短距离 d_u(只走残量大于 0 的边)。这样把网络分为若干层:

分层图只保留满足 d_v = d_u+1 的边 u \to v,即只走往下一层的边。

Dinic 在每个阶段只找最短的增广路,使得:

复杂度 O(n^2m),与 EK 算法相同的:远达不到。

代码:

#include <bits/stdc++.h>
using namespace std;
#define add(u, v, w)                           \
    vec[u].push_back({{v, w}, vec[v].size()}); \
    vec[v].push_back({{u, 0}, vec[u].size() - 1});
#define int long long
int n, m, s, t, L;
vector<pair<pair<int, int>, int>> vec[41000];
int de[41000], cu[41000], q[41000];
bool bfs()
{
    for (int i = 0; i <= t; i++)
        de[i] = 0;
    int he = 0, ta = 0;
    q[ta++] = s;
    de[s] = 1;
    while (he < ta)
    {
        int v = q[he++];
        for (int i = 0; i < (int)vec[v].size(); i++)
        {
            int to = vec[v][i].first.first;
            int ca = vec[v][i].first.second;
            if (ca && !de[to])
            {
                de[to] = de[v] + 1;
                q[ta++] = to;
            }
        }
    }
    return de[t] != 0;
}
int dfs(int v, int fl)
{
    if (v == t)
        return fl;
    int re = 0;
    for (int &i = cu[v]; i < (int)vec[v].size(); i++)
    {
        int to = vec[v][i].first.first;
        int ca = vec[v][i].first.second;
        if (ca && de[to] == de[v] + 1)
        {
            int d = dfs(to, min(fl - re, ca));
            if (d > 0)
            {
                vec[v][i].first.second -= d;
                vec[to][vec[v][i].second].first.second += d;
                re += d;
                if (re == fl)
                    return re;
            }
        }
    }
    return re;
}
int dinic()
{
    int f = 0;
    while (bfs())
    {
        for (int i = 0; i <= t; i++)
            cu[i] = 0;
        int d;
        while ((d = dfs(s, 1e18)) != 0)
        {
            f += d;
        }
    }
    return f;
}
signed main()
{
}

例题

这里会选一些老师让我们做的题目。

试题库问题

题意

大概就是 n 个题目好几个标签 b_i,现在需要第 i 个标签 a_i 个。

思路

一个题目能为一种标签贡献 1,把源点对每个题目连一条容量为 1 的边,每个题目对其拥有的标签连一条容量为 1 的边,如果这条边的流是 1 代表这个题目选了这个标签,否则就是不选,然后每个标签对汇点连一条容量为 a_i 的边表示需要 a_i 个这种标签。

对这个图跑一下最大流,若最大流不是 \sum_{i=1}^n a_i,无解,否则,对于每个标签,枚举哪些题目指向了它,输出即可。

代码:

#include <bits/stdc++.h>
using namespace std;
#define int long long
int n, m, s, t;
vector<pair<pair<int, int>, int>> vec[10005];
int vis[10005];
struct edge
{
    int v1, w1, id;
} pre[10005];
bool bfs()
{
    queue<int> que;
    memset(vis, 0, sizeof(vis));
    vis[s] = 1;
    que.push(s);
    while (!que.empty())
    {
        int v = que.front();
        que.pop();
        if (v == t)
            return 1;
        for (int i = 1; i < vec[v].size(); i++)
        {
            if (vec[v][i].first.second && !vis[vec[v][i].first.first])
            {
                vis[vec[v][i].first.first] = 1;
                pre[vec[v][i].first.first].v1 = v;
                pre[vec[v][i].first.first].w1 = vec[v][i].first.second;
                pre[vec[v][i].first.first].id = i;
                que.push(vec[v][i].first.first);
            }
        }
    }
    return 0;
}
signed main()
{
    volatile char c1;
    clock_t b = clock();
    cin >> n >> m;
    int sum = 0;
    s = 0;
    t = n + m + 1;
    for (int i = 0; i <= t; i++)
        vec[i].push_back({{0, 0}, 0});
    for (int i = 1; i <= n; i++)
    {
        int w;
        cin >> w;
        sum += w;
        vec[s].push_back({{i, w}, vec[i].size()});
        vec[i].push_back({{s, 0}, vec[s].size() - 1});
    }
    for (int i = 1; i <= m; i++)
    {
        int x;
        cin >> x;
        int q = n + i;
        vec[q].push_back({{t, 1}, vec[t].size()});
        vec[t].push_back({{q, 0}, vec[q].size() - 1});
        for (int j = 1; j <= x; j++)
        {
            int u;
            cin >> u;
            vec[u].push_back({{q, 1}, vec[q].size()});
            vec[q].push_back({{u, 0}, vec[u].size() - 1});
        }
    }
    int ans = 0;
    while (bfs())
    {
        int minn = 1e18;
        for (int i = t; i != s; i = pre[i].v1)
            minn = min(minn, pre[i].w1);
        for (int i = t; i != s; i = pre[i].v1)
        {
            vec[pre[i].v1][pre[i].id].first.second -= minn;
            vec[i][vec[pre[i].v1][pre[i].id].second].first.second += minn;
        }
        ans += minn;
    }
    if (ans != sum)
        cout << "No Solution!" << endl;
    else
    {
        for (int i = 1; i <= n; i++)
        {
            cout << i << ":";
            for (int j = 1; j < vec[i].size(); j++)
                if (vec[i][j].first.second == 0 && vec[i][j].first.first > n && vec[i][j].first.first <= n + m)
                    cout << " " << vec[i][j].first.first - n;
            cout << endl;
        }
    }
    volatile char c2;
    cerr << "Memory: " << abs(&c1 - &c2) / 1024 / 1024 << " MB" << endl;
    clock_t e = clock();
    cerr << "Time: " << (double)(e - b) * 1000 / CLOCKS_PER_SEC << " ms" << endl;
}

[POI 2005] KOS-Dicing

题意

## 思路 看到最大值最小一眼二分。 源点指向每场比赛表示每场比赛有且仅有一个人赢,每场比赛指向对应的两个玩家,若这条边的流是 $1$ 代表对应的玩家赢了,否则代表输了。 假设我们未卜先知,知道在赢得最多的人赢得最少的情况下赢得最多的人赢了 $x$ 把,则每个人赢得场数不超过 $x$,即对于每个人建一个边连向汇点,容量是 $x$,跑最大流,若最大流是 $m$,即每个比赛都有一个赢家。 考虑如何求出这个未卜先知的 $x$,显然可以二分求 $x$,那就做完了。 哦对还要输出方案,那就再跑一次最大流然后暴力查找对于每场比赛谁赢了就行。 注意有 SPJ,别管和样例一不一样。 代码: ``` #include <bits/stdc++.h> using namespace std; #define add(u, v, w) \ vec[u].push_back({{v, w}, vec[v].size()}); \ vec[v].push_back({{u, 0}, vec[u].size() - 1}); #define int long long int n, m, s, t; vector<pair<pair<int, int>, int>> vec[20005]; int de[20005], cu[20005], q[20005]; bool bfs() { for (int i = 0; i <= t; i++) de[i] = 0; int he = 0, ta = 0; q[ta++] = s; de[s] = 1; while (he < ta) { int v = q[he++]; for (int i = 1; i < (int)vec[v].size(); i++) { int to = vec[v][i].first.first; int ca = vec[v][i].first.second; if (ca && !de[to]) { de[to] = de[v] + 1; q[ta++] = to; } } } return de[t] != 0; } int dfs(int v, int fl) { if (v == t) return fl; for (int &i = cu[v]; i < (int)vec[v].size(); i++) { int to = vec[v][i].first.first; int ca = vec[v][i].first.second; if (ca && de[to] == de[v] + 1) { int d = dfs(to, min(fl, ca)); if (d > 0) { vec[v][i].first.second -= d; vec[to][vec[v][i].second].first.second += d; return d; } } } return 0; } int a[10005], b[10005]; int dinic() { int f = 0; while (bfs()) { for (int i = 0; i <= t; i++) cu[i] = 1; int d; while (d = dfs(s, 1e18)) f += d; } return f; } bool check(int x) { for (int i = 0; i <= n + m + 1; i++) { vec[i].clear(); vec[i].push_back({{0, 0}, 0}); } s = 0; t = n + m + 1; for (int i = 1; i <= m; i++) { add(s, i, 1); add(i, m + a[i], 1); add(i, m + b[i], 1); } for (int i = 1; i <= n; i++) { add(m + i, t, x); } return dinic() >= m; } signed main() { volatile char c1; clock_t b1 = clock(); cin >> n >> m; for (int i = 1; i <= m; i++) cin >> a[i] >> b[i]; int l = 1, r = m, k = m; while (l <= r) { int mid = (l + r) >> 1; if (check(mid)) { k = mid; r = mid - 1; } else l = mid + 1; } cout << k << endl; check(k); for (int i = 1; i <= m; i++) { int w = 0; for (int j = 1; j < vec[i].size(); j++) { int to = vec[i][j].first.first; int ca = vec[i][j].first.second; if (to > m && to <= m + n && ca == 0) { w = to - m; break; } } cout << (w == a[i] ? 1 : 0) << "\n"; } volatile char c2; cerr << "Memory: " << abs(&c1 - &c2) / 1024 / 1024 << " MB" << endl; clock_t e = clock(); cerr << "Time: " << (double)(e - b1) * 1000 / CLOCKS_PER_SEC << " ms" << endl; } ``` ## [\[清华集训 2012\] 最小生成树](https://www.luogu.com.cn/problem/P5934) 切了这题,就能进清华集训了吗? ## 题意 给一个图,问最少删几个边才能使得某个给定的边即在最小生成树又在最大生成树上。 ## 思路 引理:如果一条插入边 $(u,v,L)$ 后该边可能在最小生成树上,那么如果将边权小于 $L$ 的边组成一个新图,则新图不连通。 证明: 假设命题不成立:$e$ 可能出现在某棵 MST 中,且 $G_{<L}$ 中 $u, v$ 连通。 由连通性,$G_{<L}$ 中存在 $u$-$v$ 路径 $P: u=x_0 \to x_1 \to \dots \to x_k=v$,且 $P$ 上每条边权值都严格小于 $L$。 在 $G'$ 中,新边 $e$ 与路径 $P$ 拼接构成一个环 $C: u \to x_1 \to \dots \to x_k=v \to u$。环上 $P$ 的边权都 $<L$,而 $w(e)=L$,故 $e$ 是环 $C$ 中严格最大的边。 由 MST 的环性质(环上严格最大的边不会出现在任何 MST 中),$e$ 不可能出现在 $G'$ 的任何 MST 中,与假设矛盾。 故假设不成立,$G_{<L}$ 中 $u, v$ 不连通,证毕。 同理,如果一条插入边 $(u,v,L)$ 后该边可能在最大生成树上,那么如果将边权大于 $L$ 的边组成一个新图,则新图不连通。 那不就是建两个图,第一个是只有所有边权 $<L$ 的边的图,一个是所有边权 $>L$ 的图,对于每个图求使它不联通的最小割吗? 由最小割等于最大流,对于两个图分别跑最大流相加即可。 代码: ``` #include <bits/stdc++.h> using namespace std; #define add(u, v, w) \ vec[u].push_back({{v, w}, vec[v].size()}); \ vec[v].push_back({{u, 0}, vec[u].size() - 1}); #define int long long int n, m, s, t, L; vector<pair<pair<int, int>, int>> vec[200005]; int de[200005], cu[200005], q[200005]; bool bfs() { for (int i = 0; i <= n; i++) de[i] = 0; int he = 0, ta = 0; q[ta++] = s; de[s] = 1; while (he < ta) { int v = q[he++]; for (int i = 0; i < (int)vec[v].size(); i++) { int to = vec[v][i].first.first; int ca = vec[v][i].first.second; if (ca && !de[to]) { de[to] = de[v] + 1; q[ta++] = to; } } } return de[t] != 0; } int dfs(int v, int fl) { if (v == t) return fl; int re = 0; for (int &i = cu[v]; i < (int)vec[v].size(); i++) { int to = vec[v][i].first.first; int ca = vec[v][i].first.second; if (ca && de[to] == de[v] + 1) { int d = dfs(to, min(fl - re, ca)); if (d > 0) { vec[v][i].first.second -= d; vec[to][vec[v][i].second].first.second += d; re += d; if (re == fl) return re; } } } return re; } int dinic() { int f = 0; while (bfs()) { for (int i = 0; i <= n; i++) cu[i] = 0; int d; while (d = dfs(s, 1e18)) f += d; } return f; } void cl() { for (int i = 0; i <= n; i++) vec[i].clear(); } struct edge { int u, v, w; } e[200005]; signed main() { ios::sync_with_stdio(false); cin.tie(0); cin >> n >> m; for (int i = 1; i <= m; i++) cin >> e[i].u >> e[i].v >> e[i].w; cin >> s >> t >> L; for (int i = 1; i <= m; i++) if (e[i].w < L) { add(e[i].u, e[i].v, 1); add(e[i].v, e[i].u, 1); } int r1 = dinic(); for(int i = 1;i <= m;i++){ vec[i].clear(); } cl(); for (int i = 1; i <= m; i++) if (e[i].w > L) { add(e[i].u, e[i].v, 1); add(e[i].v, e[i].u, 1); } int r2 = dinic(); cout << r1 + r2 << endl; } ``` ## [\[ABC239G\] Builder Takahashi](https://www.luogu.com.cn/problem/AT_abc239_g) ### 题意 给定一张 $n$ 个点 $m$ 条边的连通无向图,要求在某些点(不能为 $1$ 号点或者 $n$ 号点)设立障碍,在 $i$ 号点建立障碍的费用为 $c_i$,要使得 $1$ 号点和 $n$ 号点不连通,求最小花费的方案。 ## 思路 这不还是让一张图不联通?上一题的弱化版,最小割等于最大流,秒了秒了。 代码: ``` #include <bits/stdc++.h> using namespace std; #define add(u, v, w) \ vec[u].push_back({{v, w}, vec[v].size()}); \ vec[v].push_back({{u, 0}, vec[u].size() - 1}); #define int long long int n, m, s, t, L; vector<pair<pair<int, int>, int>> vec[20005]; int de[20005], cu[20005], q[20005]; bool bfs() { for (int i = 0; i <= n; i++) de[i] = 0; int he = 0, ta = 0; q[ta++] = s; de[s] = 1; while (he < ta) { int v = q[he++]; for (int i = 0; i < (int)vec[v].size(); i++) { int to = vec[v][i].first.first; int ca = vec[v][i].first.second; if (ca && !de[to]) { de[to] = de[v] + 1; q[ta++] = to; } } } return de[t] != 0; } int dfs(int v, int fl) { if (v == t) return fl; int re = 0; for (int &i = cu[v]; i < (int)vec[v].size(); i++) { int to = vec[v][i].first.first; int ca = vec[v][i].first.second; if (ca && de[to] == de[v] + 1) { int d = dfs(to, min(fl - re, ca)); if (d > 0) { vec[v][i].first.second -= d; vec[to][vec[v][i].second].first.second += d; re += d; if (re == fl) return re; } } } return re; } int dinic() { int f = 0; while (bfs()) { for (int i = 0; i <= n; i++) cu[i] = 0; int d; while (d = dfs(s, 1e18)) f += d; } return f; } int c[105], nn; signed main() { ios::sync_with_stdio(false); cin.tie(0); cin >> nn >> m; for (int i = 1; i <= m; i++) { int a, b; cin >> a >> b; add(a + nn, b, 1e18); add(b + nn, a, 1e18); } for (int i = 1; i <= nn; i++) cin >> c[i]; for (int i = 2; i <= nn - 1; i++) { add(i, i + nn, c[i]); } s = 1 + nn; t = nn; n = 2 * nn; int r = dinic(); cout << r << "\n"; bfs(); vector<int> an; for (int i = 2; i <= nn - 1; i++) if (de[i] && !de[i + nn]) an.push_back(i); cout << an.size() << "\n"; for (int i : an) cout << i << " "; if (an.empty()) cout << "\n"; } ``` ## [方格取数问题](https://www.luogu.com.cn/problem/P2774) 这个题超级有意思!有六倍经验($2$ 紫 $4$ 蓝)。 ## 题意 有一个 $m$ 行 $n$ 列的方格图,每个方格中都有一个正整数。现要从方格中取数,使任意两个数所在方格没有公共边,且取出的数的总和最大,请求出最大的和。 ## 思路 考虑将网格黑板染色,先把源点连每个黑点,容量是点权,把每个黑点连周围的四个白的,容量没有意义,设为 inf,将每个白点连汇点,容量为点权,既然连了边的黑点白点只能留一个,那就是典型的断链,最小割即可。 最小割等于最大流,按上面的方法建图跑最大流就做完了。 代码: ``` #include <bits/stdc++.h> using namespace std; #define add(u, v, w) \ vec[u].push_back({{v, w}, vec[v].size()}); \ vec[v].push_back({{u, 0}, vec[u].size() - 1}); #define int long long int n, m, s, t, L; vector<pair<pair<int, int>, int>> vec[20005]; int de[20005], cu[20005], q[20005]; bool bfs() { for (int i = 0; i <= t; i++) de[i] = 0; int he = 0, ta = 0; q[ta++] = s; de[s] = 1; while (he < ta) { int v = q[he++]; for (int i = 0; i < (int)vec[v].size(); i++) { int to = vec[v][i].first.first; int ca = vec[v][i].first.second; if (ca && !de[to]) { de[to] = de[v] + 1; q[ta++] = to; } } } return de[t] != 0; } int dfs(int v, int fl) { if (v == t) return fl; int re = 0; for (int &i = cu[v]; i < (int)vec[v].size(); i++) { int to = vec[v][i].first.first; int ca = vec[v][i].first.second; if (ca && de[to] == de[v] + 1) { int d = dfs(to, min(fl - re, ca)); if (d > 0) { vec[v][i].first.second -= d; vec[to][vec[v][i].second].first.second += d; re += d; if (re == fl) return re; } } } return re; } int dinic() { int f = 0; while (bfs()) { for (int i = 0; i <= t; i++) cu[i] = 0; int d; while ((d = dfs(s, 1e18)) != 0) { f += d; } } return f; } int a[1000][1000], nn; signed main() { ios::sync_with_stdio(false); cin.tie(0); int sum = 0; cin >> m >> n; s = 0, t = n * m + n + 1; for (int i = 1; i <= m; i++) for (int j = 1; j <= n; j++) { cin >> a[i][j]; sum += a[i][j]; } for (int i = 1; i <= m; i++) for (int j = 1; j <= n; j++) { int id = (i - 1) * n + j; if ((i + j) & 1) { add(s, id, a[i][j]); if (i > 1) add(id, id - n, 0x3f3f3f3f); if (i < m) add(id, id + n, 0x3f3f3f3f); if (j > 1) add(id, id - 1, 0x3f3f3f3f); if (j < n) add(id, id + 1, 0x3f3f3f3f); } else add(id, t, a[i][j]); } int fl = dinic(); cout << sum - fl << endl; } ``` 好了,讲一讲六倍经验吧! ### 二倍经验[王者之剑](https://www.luogu.com.cn/problem/P4474) #### 题意 化简后题意和上一题一样,不再过多阐述。 #### 思路 和上一题完全一样。 代码:和上一题完全一样。 ### 三倍经验[骑士放置](https://www.luogu.com.cn/problem/P10939) #### 题意 一个网格,有一些地方不能放东西,问其他地方最多放多少骑士。 注:骑士的行走规则和象棋种的马相同。 #### 思路 将上一题的思路转化一下,黑格连白格变为每个点连若他是骑士可以跳到的点,然后做完了。 代码: ``` #include <bits/stdc++.h> using namespace std; #define add(u, v, w) \ vec[u].push_back({{v, w}, vec[v].size()}); \ vec[v].push_back({{u, 0}, vec[u].size() - 1}); #define int long long int n, m, s, t, L; vector<pair<pair<int, int>, int>> vec[20005]; int de[20005], cu[20005], q[20005]; bool bfs() { for (int i = 0; i <= t; i++) de[i] = 0; int he = 0, ta = 0; q[ta++] = s; de[s] = 1; while (he < ta) { int v = q[he++]; for (int i = 0; i < (int)vec[v].size(); i++) { int to = vec[v][i].first.first; int ca = vec[v][i].first.second; if (ca && !de[to]) { de[to] = de[v] + 1; q[ta++] = to; } } } return de[t] != 0; } int dfs(int v, int fl) { if (v == t) return fl; int re = 0; for (int &i = cu[v]; i < (int)vec[v].size(); i++) { int to = vec[v][i].first.first; int ca = vec[v][i].first.second; if (ca && de[to] == de[v] + 1) { int d = dfs(to, min(fl - re, ca)); if (d > 0) { vec[v][i].first.second -= d; vec[to][vec[v][i].second].first.second += d; re += d; if (re == fl) return re; } } } return re; } int dinic() { int f = 0; while (bfs()) { for (int i = 0; i <= t; i++) cu[i] = 0; int d; while ((d = dfs(s, 1e18)) != 0) { f += d; } } return f; } char a[105][105]; int dx[] = {1, 1, 2, 2, -1, -1, -2, -2}; int dy[] = {2, -2, 1, -1, 2, -2, 1, -1}; signed main() { ios::sync_with_stdio(false); cin.tie(0); int nn, mm, kk; cin >> nn >> mm >> kk; n = nn; m = mm; s = 0, t = n * m + m + 1; for (int i = 1; i <= kk; i++) { int x, y; cin >> x >> y; a[x][y] = 1; } int cnt = nn * mm - kk; for (int i = 1; i <= nn; i++) for (int j = 1; j <= mm; j++) { if (a[i][j]) continue; int id = (i - 1) * mm + j; if ((i + j) & 1) { add(s, id, 1); for (int k = 0; k < 8; k++) { int ni = i + dx[k], nj = j + dy[k]; if (ni < 1 || ni > nn || nj < 1 || nj > mm) continue; if (a[ni][nj]) continue; add(id, (ni - 1) * mm + nj, 0x3f3f3f3f); } } else add(id, t, 1); } int fl = dinic(); cout << cnt - fl << endl; } ``` ### 四倍经验 [\[TJOI2013\] 攻击装置](https://www.luogu.com.cn/problem/P4304) 和上题一模一样,输入方式变了变,不再用过多篇幅阐述,代码也不放了。 ### 五倍经验[骑士共存问题](https://www.luogu.com.cn/problem/P3355) 还是一样的,输入方式变了变。 ### 六倍经验[长脖子鹿放置](https://www.luogu.com.cn/problem/P5030) 还是一样的,不过是改了改连边的坐标。 代码不放了。 ## [太空飞行计划问题](https://www.luogu.com.cn/problem/P2762) ### 题意 每个实验有对应的奖金和需要的设备(多个),你可以选择做和不做,获得相应的收益,最大化奖金总和减去买设备的钱。 ### 思路 所有的实验与源点相连,容量为其奖金,所有的器材与汇点相连,容量为其价格,中间实验与器材相连,容量为无穷大。无穷大时绝对割不掉的,要么割奖金,要么割设备,由于要求收益最大,所以是最小割,最小割等于最大流,跑最大流即可。 代码: ``` #include <bits/stdc++.h> using namespace std; #define add(u, v, w) \ vec[u].push_back({{v, w}, vec[v].size()}); \ vec[v].push_back({{u, 0}, vec[u].size() - 1}); #define int long long int n, m, s, t, L; vector<pair<pair<int, int>, int>> vec[20005]; int de[20005], cu[20005], q[20005]; bool bfs() { for (int i = 0; i <= t; i++) de[i] = 0; int he = 0, ta = 0; q[ta++] = s; de[s] = 1; while (he < ta) { int v = q[he++]; for (int i = 0; i < (int)vec[v].size(); i++) { int to = vec[v][i].first.first; int ca = vec[v][i].first.second; if (ca && !de[to]) { de[to] = de[v] + 1; q[ta++] = to; } } } return de[t] != 0; } int dfs(int v, int fl) { if (v == t) return fl; int re = 0; for (int &i = cu[v]; i < (int)vec[v].size(); i++) { int to = vec[v][i].first.first; int ca = vec[v][i].first.second; if (ca && de[to] == de[v] + 1) { int d = dfs(to, min(fl - re, ca)); if (d > 0) { vec[v][i].first.second -= d; vec[to][vec[v][i].second].first.second += d; re += d; if (re == fl) return re; } } } return re; } int dinic() { int f = 0; while (bfs()) { for (int i = 0; i <= t; i++) cu[i] = 0; int d; while ((d = dfs(s, 1e18)) != 0) { f += d; } } return f; } char a[105][105]; int dx[] = {1, 1, 2, 2, -1, -1, -2, -2}; int dy[] = {2, -2, 1, -1, 2, -2, 1, -1}; signed main() { cin >> m >> n; s = 0; t = m + n + 1; int sum = 0; for (int i = 1; i <= m; i++) { int u, v, w; cin >> u; sum += u; add(s, i, u); char tools[10000]; memset(tools, 0, sizeof tools); cin.getline(tools, 10000); int ulen = 0, tool; while (sscanf(tools + ulen, "%d", &tool) == 1) // 之前已经用scanf读完了赞助商同意支付该实验的费用 { // tool是该实验所需仪器的其中一个 // 这一行,你可以将读进来的编号进行储存、处理,如连边。 add(i, m + tool, 0x3f3f3f3f); if (tool == 0) ulen++; else { while (tool) { tool /= 10; ulen++; } } ulen++; } } for (int i = 1; i <= n; i++) { int jia; cin >> jia; add(m + i, t, jia); } int fl = dinic(); for (int i = 1; i <= m; i++) { if (de[i]) { cout << i << " "; } } cout << endl; for (int i = 1; i <= n; i++) { if (de[i + m]) { cout << i << " "; } } cout << endl; cout << sum - fl << endl; } ``` --- 关于我为什么写网络流的笔记:我线上听课,别人线下,然后我们分为两堂课有课间休息,第一堂老师没有共享屏幕,听了一节课老师的声音以感性理解,第二堂老师没有开麦,看了一节课老师在黑板上画乱七八糟的图案,啥也没听懂。