网络流是个很有用的东西,很多问题都可以转化为网络流。
目前本文会很快切入建图,证明什么有时间再补。
概述
一个网络是一张有向图 ,对于每条有向边 存在容量限制 ,当 时,。网络的可行流分为有源汇(指定了两个节点 ,代表图的源点和汇点)和无源汇,但是都存在一个定义域为节点二元组的流函数 , 代表边 的流量,满足:
- 满足容量限制:,当两者相等时, 就流满了;
- 斜对称性质:,也就是说,反向边其实是负的流量[1];
- 流量守恒:除源点和汇点外(当然只限于有源汇可以除了这两个节点),从每个节点流入和流出的流量相等,即 ,也就是说,每个节点不储存流量,进去多少就流出来多少。
以下是一些定义:
- 对于有源汇,有 ,此时这个相等的和成为当前流 的流量。
- 定义流 在网络 上的残量网络 为容量函数 。根据容量限制,,当 时,则视为 在残量网络上不存在。也就是说,在残量网络中我们要删掉满流边。
- 定义增广路 是残量网络 上源点到汇点的一条路径,而无源汇则没有增广路。
- 将点集分为两个互补相交的 ,且满足 ,这种划分方式称为割,割的容量为 ,流量为 。如果 ,那么 是割边,可以看出,割边一般不止一条。
接下来讨论的内容默认都是有源流!
最大流问题
最大流是求一个网络的最大流量。
增广
找到一条残量网络 上的增广路 ,并为 上的每一条边增加 的流量。也就是说,流量能流满就能流满。这一过程称之为增广。
怎样证明这个贪心策略?不需要!实际上在当前边的流量增加 时,我们要给它的反向边的容量增加 ,其实这是一个反悔贪心的策略,可以回收原来的流量,正确性得以保证。
最大流最小割定理
割中割边容量和最小的划分方式称为最小割,而且最大流等于最小割。证明如下:
- 存在一组流的流量等于一组割的容量:只需要让割边流满即可,最大流存在的时候残量网络不连通,也就提供了一组割。
- 任意一组流的流量不大于任意一组割的容量。如果割掉这些边,那么网络流量尚未最大化,仍然可以找到增广路,与最大流的求解方式矛盾。
Dinic 算法
Edmonds-Karp 和其他高级算法在本文中不会出现,只会介绍 Dinic。
模板。
Dinic 会不断进行 bfs 来在残量网络上构造分层图(根据节点的层次 ,从 到 最少需要经过的边数),然后在分层图上进行 dfs 寻找增广路。时间复杂度为 。
查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long i64;
const i64 INF = 1e18;
struct edge {
int u, v, w;
edge(int u = 0, int v = 0, int w = 0) : u(u), v(v), w(w) {}
};
int n, m, s, t, d[1205], cur[1205]; // cur 为当前弧下标
vector<int> G[1205];
vector<edge> edges;
inline void addedge(int u, int v, int w) {
edges.emplace_back(edge(u, v, w));
G[u].emplace_back(edges.size() - 1);
}
bool bfs(void) {
memset(cur, 0, sizeof(cur));
memset(d, -1, sizeof(d));
queue<int> q;
q.push(s); d[s] = 1;
while (!q.empty()) {
int u = q.front(); q.pop();
for (int i = 0; i < G[u].size(); ++i) {
edge &e = edges[G[u][i]];
if (e.w && d[e.v] == -1) {
q.push(e.v);
d[e.v] = d[u] + 1;
if (e.v == t) return true;
}
}
}
return false;
}
i64 dinic(int x, i64 res) {
if (x == t || res == 0) return res;
i64 flow = 0;
for (int i = cur[x]; i < G[x].size() && res; ++i) {
edge &e = edges[G[x][i]]; cur[x] = i;
i64 c = min(res, (i64)e.w);
if (d[e.v] == d[x] + 1 && c) {
i64 k = dinic(e.v, c);
flow += k; res -= k;
edges[G[x][i]].w -= k; edges[G[x][i] ^ 1].w += k;
}
}
if (!flow) d[x] = -1;
return flow;
}
int main(void) {
scanf("%d%d%d%d", &n, &m, &s, &t);
while (m--) {
int u, v, w; scanf("%d%d%d", &u, &v, &w);
addedge(u, v, w); addedge(v, u, 0);
}
i64 flow = 0, maxflow = 0;
while (bfs()) while (flow = dinic(s, INF)) maxflow += flow;
printf("%lld\n", maxflow);
return 0;
}
无负环费用流
指的是最小费用最大流,也就是说,在原有网络的基础上,每一条边多了一个权值 ,要求在保证最大流前提下,找出 的最小值。通常使用 SPP 算法解决这个问题。
SSP 实现时只需要将 Dinic 的 bfs 替换为 SPFA 即可(每条边的长度为权值 )。相应的地方改一下即可。理论上限时间复杂度很高,但是实际表现不错,而且业界公约不卡。
模板,代码如下:
查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long i64;
const i64 INF = 1e18;
struct edge {
int u, v, w, c;
edge(int u = 0, int v = 0, int w = 0, int c = 0) :
u(u), v(v), w(w), c(c) {}
};
int n, m, s, t, cur[5005], d[5005];
bool inq[5005];
vector<int> G[5005];
vector<edge> edges;
inline void addedge(int u, int v, int w, int c) {
edges.emplace_back(edge(u, v, w, c));
G[u].emplace_back(edges.size() - 1);
}
bool SPFA(void) {
memset(cur, 0, sizeof(cur));
memset(d, 0x3f, sizeof(d));
queue<int> q; q.push(s); d[s] = 0; inq[s] = true;
while (!q.empty()) {
int u = q.front(); q.pop(); inq[u] = false;
for (int i = 0; i < G[u].size(); ++i) {
edge &e = edges[G[u][i]];
if (e.w && d[e.v] > d[u] + e.c) {
d[e.v] = d[u] + e.c;
if (!inq[e.v]) q.push(e.v), inq[e.v] = true;
}
}
}
return d[t] < 0x3f3f3f3f;
}
i64 cost;
bool vis[5005];
i64 dinic(int x, i64 res) {
if (x == t) return res;
i64 flow = 0; vis[x] = true;
for (int i = cur[x]; i < G[x].size() && res; ++i) {
edge &e = edges[G[x][i]]; cur[x] = i;
i64 c = min(res, i64(e.w));
if (!vis[e.v] && c && d[e.v] == d[x] + e.c) {
i64 k = dinic(e.v, c);
flow += k; cost += k * e.c; res -= k;
edges[G[x][i]].w -= k; edges[G[x][i] ^ 1].w += k;
}
}
if (!flow) d[x] = 0x3f3f3f3f;
vis[x] = false;
return flow;
}
int main(void) {
scanf("%d%d%d%d", &n, &m, &s, &t);
while (m--) {
int u, v, w, c; scanf("%d%d%d%d", &u, &v, &w, &c);
addedge(u, v, w, c); addedge(v, u, 0, -c);
}
i64 maxflow = 0, flow;
while (SPFA()) while (flow = dinic(s, INF)) maxflow += flow;
printf("%lld %lld\n", maxflow, cost);
return 0;
}
解题思路
网络流本质上是一种反悔贪心,将贪心的反悔策略用一条反向边简单的表示。在解题时,关键在于找到题目中的方案和一种流或割对应。我们来看一个例子:
幼儿园里有 个小朋友打算通过投票来决定睡不睡午觉。
虽然每个人都有自己的主见,但是为了照顾一下自己朋友的想法,他们也可以投和自己本来意愿相反的票。
我们定义一次投票的冲突数为好朋友之间发生冲突的总数加上和所有和自己本来意愿发生冲突的人数。
给出每个小朋友的意愿和每一对好朋友。我们的问题就是,每位小朋友应该怎样投票,才能使冲突数最小?输出可能的最小冲突数。
对于 的数据,,。
这看起来很像一个最小割的模型,考虑如何使用一组割来表示一种意见方案。每割掉一条边需要付出 的代价。于是将支持的小朋友与 连边,反对的小朋友与 连边,每对朋友之间连边,求出图的最小割即可。这其实是一个集合划分模型。
查看代码
#include <bits/stdc++.h>
using namespace std;
int n, m;
struct edge {
int u, v, w;
edge(int u = 0, int v = 0, int w = 0) : u(u), v(v), w(w) {}
};
vector<edge> edges;
vector<int> G[305];
int d[305], cur[305];
inline void addedge(int u, int v, int w) {
edges.emplace_back(edge(u, v, w));
G[u].emplace_back(edges.size() - 1);
}
bool bfs(void) {
memset(d, -1, sizeof(d)); memset(cur, 0, sizeof(cur));
queue<int> q; q.push(0); d[0] = 1;
while (!q.empty()) {
int u = q.front(); q.pop();
for (int i = 0; i < G[u].size(); ++i) {
edge &e = edges[G[u][i]];
if (e.w && d[e.v] == -1) {
q.push(e.v); d[e.v] = d[u] + 1;
if (e.v == n + 1) return true;
}
}
}
return false;
}
int dinic(int x, int res) {
if (x == n + 1 || res == 0) return res;
int flow = 0;
for (int i = cur[x]; i < G[x].size(); ++i) {
edge &e = edges[G[x][i]]; cur[x] = i;
int c = min(res, e.w);
if (d[e.v] == d[x] + 1 && c) {
int k = dinic(e.v, c);
flow += k; res -= k;
edges[G[x][i]].w -= k; edges[G[x][i] ^ 1].w += k;
}
}
if (!flow) d[x] = -1;
return flow;
}
int type[305];
int main(void) {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i) {
scanf("%d", type + i);
if (type[i]) addedge(0, i, 1), addedge(i, 1, 0);
else addedge(i, n + 1, 1), addedge(n + 1, i, 0);
}
while (m--) {
int u, v; scanf("%d%d", &u, &v);
if (type[u] == 0 && type[v] == 1) swap(u, v);
addedge(u, v, 1); addedge(v, u, 0);
}
int flow = 0, maxflow = 0;
while (bfs()) while (flow = dinic(0, 1e9)) maxflow += flow;
printf("%d\n", maxflow);
return 0;
}
如果要求输出集合划分模型的一组方案,那么就看残余网络上的 到每个点的距离。如果能走到就是在 ,否则就是在 。
上下界网络流
有的题目的流量限制除了有上界,还有下界。
网络流常见模型
我们来看一些网络流的常见模型。
点边转化 | 最小割点
如果删去点 有代价 ,求使得 不连通的最小代价?
将每个点拆成入点 和出点 ,在它们之间连一条容量为 的边,表示删去这个点。对于原图的每一条边 ,连接 一条 容量的边,这样我们只能删点而不会割边。
集合划分模型
这个内容非常常见。
说的就是为 选定合适的值,使得和式的值最小。
连 的容量为 的边, 的容量为 的边。如果割掉了 ,说明将 划分到了集合 ,代价为 。
给 连容量限制为 的双向边,这样如果 不属于同一集合,至少会割掉其中一条边。
这样这个网络的最小割就是答案。
还有一种限制: 在集合 且 在集合 时有代价 ,那么连 的容量为 的边,这样如果 和 相连且 和 相连,那么这条边需要被割掉。
如何输出方案?要注意什么是割边:在最后的残量网络上 是否存在,存在则属于集合 ,否则是集合 。
最大权闭合子图
一张有向图 的闭合子图 定义在点集 上,一个点集 符合要求当且仅当 内部每个点的所有出边仍指向 ,即点集内部每个点在有向图上能够到达的点仍然属于该点集。
而最大权闭合子图则是每个点有点权,求闭合子图的最大权值。
考虑集合划分模型,对于每个节点,可以将其划分到选或不选的集合中,也就是 连 , 连 。如果 ,就是如果 分到选的集合中, 也必须分到选的集合中,即 有容量 ,求网络的最大割即可。
但是最大割是 NPH 的,因此考虑取相反数求最小割。然而这样会出现负容量的边!将负容量的边改到 来连。这样答案为所有正权值的和减去最小割即可。
模板,代码如下:
查看代码
#include <bits/stdc++.h>
using namespace std;
int m, n, ans;
struct edge {
int v, w;
edge(int v = 0, int w = 0) : v(v), w(w) {}
};
struct Graph {
int S, T, cur[205], d[205];
vector<edge> edges;
vector<int> G[205];
inline void addedge(int u, int v, int w) {
edges.emplace_back(edge(v, w));
G[u].emplace_back(edges.size() - 1);
edges.emplace_back(edge(u, 0));
G[v].emplace_back(edges.size() - 1);
}
bool bfs(void) {
memset(cur, 0, sizeof cur); memset(d, -1, sizeof d);
queue<int> q; q.push(S); d[S] = 1;
while (!q.empty()) {
int u = q.front(); q.pop();
for (int i = 0; i < G[u].size(); ++i) {
edge &e = edges[G[u][i]];
if (e.w && d[e.v] == -1) {
d[e.v] = d[u] + 1; q.push(e.v);
if (e.v == T) return true;
}
}
}
return false;
}
int dinic(int x, int res) {
if (x == T) return res;
int flow = 0;
for (int i = cur[x]; i < G[x].size() && res; ++i) {
edge &e = edges[G[x][i]]; cur[x] = i;
int c = min(res, e.w);
if (d[e.v] == d[x] + 1 && c) {
int k = dinic(e.v, c); flow += k; res -= k;
edges[G[x][i]].w -= k; edges[G[x][i] ^ 1].w += k;
}
}
if (!flow) d[x] = -1;
return flow;
}
int maxFlow(int s, int t) {
S = s, T = t;
int flow, maxflow = 0;
while (bfs()) while (flow = dinic(S, 1e9)) maxflow += flow;
return maxflow;
}
} G;
int main(void) {
scanf("%d%d", &m, &n);
for (int i = 1; i <= m; ++i) {
int w, x; scanf("%d", &w); G.addedge(0, i, w); ans += w;
while (scanf("%d", &x) == 1 && x) G.addedge(i, x + m, 1e9);
}
for (int i = 1, w; i <= n; ++i) scanf("%d", &w), G.addedge(i + m, n + m + 1, w);
printf("%d\n", ans - G.maxFlow(0, n + m + 1));
return 0;
}
二分图
设无向图 ,若能够将 分成两个点集 满足 且 (也可以反过来),这样 是一张二分图, 分别称为左部点和右部点。
二分图的充要条件是不存在奇环,这样我们可以给二分图进行黑白染色。
从某个点开始 DFS,遍历当前点 和邻居 ,如果 未被访问,则 的颜色与 相反;如果访问过,说明存在奇环。
二分图匹配
给定二分图 ,若边集 满足 中任意两条边不交于同一端点,则称 是 的一组匹配,其大小为 。
特别的,若 ,则称 为完美匹配。
最大匹配
我们希望求出边集 的最大大小。
显然,我们从 向 的所有点连一条 的边, 向 连一条 的边,根据 从 向 连边,跑最大流即可。在这里 Dinic 的时间复杂度为 。
最大多重匹配
即节点 不能与超过 条边相连,这样只需要将每个点与源点或汇点的容量设为 即可。时间复杂度依然正确。
匈牙利算法
匈牙利算法可以以 的时间复杂度解决二分图的最大匹配问题。
工作过程如下:
- 加入一个左部点 ,然后让 去尝试匹配。
- 如果 ,已经匹配,则增广失败。
- 遍历 能到达的所有右部点 :
- 被访问过了,那么直接再见。
- 没被匹配,让 匹配 ,增广成功。
- 被匹配,考虑 原来匹配的 ,如果 还能够成功匹配,那么改为让 匹配 ,形成新的增广路。
实际上,如果当前点失配,vis
数组是不需要清空的!模板,代码如下:
int n, m, E, mch[505], vis[505];
vector<int> G[505];
bool dfs(int x) {
if (vis[x]) return 0; vis[x] = 1;
for (int y : G[x])
if (!mch[y] || dfs(mch[y])) return mch[y] = x, 1;
return 0;
}
int main(void) {
cin >> n >> m >> E;
for(int i = 1; i <= E; i++) {
int u, v; cin >> u >> v;
G[u].push_back(v);
}
int ans = 0;
for (int i = 1; i <= n; i++) if (dfs(i)) {
memset(vis, 0, sizeof(vis));
++ans;
}
cout << ans << endl;
return 0;
}
二分图相关问题
二分图有一些常见问题。我们来看:
最小点覆盖集
给定二分图 ,若点集 满足对于任意 都有 或 ,则称 是 的点覆盖集。
考虑一组点覆盖集,不存在边 使得 同时不属于 ,这是集合划分模型?但是有问题,我们要设置在同一个集合是代价是无穷的,集合划分模型没有这样的操作。
但是 是二分图,任意一条边连接两部点,因此考虑将一部分点的状态取反。即左部点与 不连通表示它不属于 ,但右部点与 连通表示它属于 。这样限制就会变为,如果左部点 与 连通, 之间有连边,但是右部点 与 连通,则 同时不属于 ,不合法。
我们将两部点之间的连边容量设为 ,对该网络求最大流,就是最小点覆盖集大小。由于每个点最多流入或流出 的流量,因此两部点之间的连边容量可以设为 ,这证明了最小点覆盖集大小等于最大匹配。
它可以求解这样的问题:对于每条限制 恰有两种方案 能满足,求最少需要选择多少种方案满足所有限制。
最大独立集
给定二分图 ,若点集 满足任意两点不直接相连,则称 是 的独立集。二分图最大独立集等于 减去最小点覆盖集。
模板,代码如下:
查看代码
#include <bits/stdc++.h>
using namespace std;
int n, m;
struct edge {
int v, w;
edge(int v = 0, int w = 0) : v(v), w(w) {}
};
struct Graph {
int s, t, cur[1005], d[1005];
vector<edge> edges;
vector<int> G[1005];
inline void addedge(int u, int v, int w) {
edges.emplace_back(v, w);
G[u].emplace_back(edges.size() - 1);
edges.emplace_back(u, 0);
G[v].emplace_back(edges.size() - 1);
}
bool bfs(void) {
memset(cur, 0, sizeof cur);
memset(d, -1, sizeof d);
queue<int> q; q.push(s); d[s] = 1;
while (!q.empty()) {
int u = q.front(); q.pop();
for (int i = 0; i < G[u].size(); ++i) {
edge &e = edges[G[u][i]];
if (e.w && d[e.v] == -1) {
q.push(e.v); d[e.v] = d[u] + 1;
if (e.v == t) return 1;
}
}
}
return 0;
}
int dinic(int x, int res) {
if (x == t) return res;
int flow = 0;
for (int i = cur[x]; i < G[x].size(); ++i) {
edge &e = edges[G[x][i]]; cur[x] = i;
int c = min(res, e.w);
if (d[e.v] == d[x] + 1 && c) {
int k = dinic(e.v, c);
flow += k; res -= k;
edges[G[x][i]].w -= k; edges[G[x][i] ^ 1].w += k;
}
}
if (!flow) d[x] = -1;
return flow;
}
int maxflow(int S, int T) {
s = S, t = T;
int maxflow = 0, flow = 0;
while (bfs()) while (flow = dinic(s, 1e9)) maxflow += flow;
return maxflow;
}
} G;
int col[1005];
vector<int> E[1005];
void dfs(int x) {
if (col[x]) G.addedge(0, x, 1);
else G.addedge(x, n + 1, 1);
for (int y : E[x]) {
if (col[y] == -1) col[y] = col[x] ^ 1, dfs(y);
if (col[x]) G.addedge(x, y, 1);
}
}
int main(void) {
scanf("%d%d", &n, &m);
while (m--) {
int u, v; scanf("%d%d", &u, &v); ++u; ++v;
E[u].emplace_back(v); E[v].emplace_back(u);
}
memset(col, -1, sizeof col);
for (int i = 1; i <= n; ++i)
if (col[i] == -1) col[i] = 1, dfs(i);
printf("%d\n", n - G.maxflow(0, n + 1));
return 0;
}
Problemset
最重要的还是那些经典模型,然后进行大量题目训练。
网络流 24 题
没有固定的顺序(但基本上按照难度排序)。虽然非常老,但是其中有些建模方式相当经典。
软件补丁问题
状压来表示点即可,压根就不是网络流。
查看代码
#include <bits/stdc++.h>
using namespace std;
int n, m, d[1100005];
bool inq[1100005];
char s[25];
struct edge {
int u1, u2, v1, v2, t;
} e[105];
queue<int> q;
int main(void) {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++i) {
scanf("%d%s", &e[i].t, s);
for (int j = 0; j < n; ++j) {
int t = (s[j] == '+' ? 1 : (s[j] == '-' ? 2 : 0));
if (t == 1) e[i].u1 |= (1 << j);
if (t == 2) e[i].u2 |= (1 << j);
}
scanf("%s", s);
for (int j = 0; j < n; ++j) {
int t = (s[j] == '+' ? 2 : (s[j] == '-' ? 1 : 0));
if (t == 1) e[i].v1 |= (1 << j);
if (t == 2) e[i].v2 |= (1 << j);
}
}
q.push((1 << n) - 1); inq[(1 << n) - 1] = true;
memset(d, 0x3f, sizeof(d)); d[(1 << n) - 1] = 0;
while (!q.empty()) {
int x = q.front(); q.pop(); inq[x] = false;
for (int i = 1; i <= m; ++i)
if ((x & e[i].u1) == e[i].u1 && (x & e[i].u2) == 0) {
int y = (x | e[i].v1 | e[i].v2) ^ e[i].v1;
if (d[y] > d[x] + e[i].t) {
d[y] = d[x] + e[i].t;
if (!inq[y]) q.push(y), inq[y] = true;
}
}
}
if (d[0] == 0x3f3f3f3f) puts("0");
else printf("%d\n", d[0]);
return 0;
}
孤岛营救问题
将钥匙状压后 bfs 最短路即可,压根不是网络流。
查看代码
#include <bits/stdc++.h>
using namespace std;
const int INF = 0x3f3f3f3f;
const int DX[4] = {-1, 0, 0, 1}, DY[4] = {0, -1, 1, 0};
int n, m, p, k, S;
int key[15][15], e[15][15][15][15], d[15][15][2050];
struct Node { int x, y, msk, d; };
int main(void) {
memset(e, -1, sizeof e);
scanf("%d%d%d%d", &n, &m, &p, &k);
for (int i = 1; i <= k; ++i) {
int a, b, c, d, x; scanf("%d%d%d%d%d", &a, &b, &c, &d, &x); --a; --b; --c; --d;
e[a][b][c][d] = e[c][d][a][b] = x;
} cin >> S;
for (int i = 1; i <= S; ++i) {
int a, b, x; scanf("%d%d%d", &a, &b, &x);
key[a - 1][b - 1] |= 1 << x - 1;
}
queue<Node> q;
memset(d, 0x3f, sizeof(d));
d[0][0][key[0][0]] = 0;
q.push({0, 0, key[0][0], 0});
while (!q.empty()) {
Node u = q.front(); q.pop();
if (u.x == n - 1 && u.y == m - 1) return printf("%d\n", u.d), 0;
for (int dir = 0; dir < 4; ++dir) {
int x = u.x + DX[dir], y = u.y + DY[dir];
if (x < 0 || y < 0 || x >= n || y >= m) continue;
int v = e[u.x][u.y][x][y], msk = u.msk | key[x][y];
if (d[x][y][msk] < INF) continue;
if (!v || v > 0 && !(u.msk >> v - 1 & 1)) continue;
q.push({x, y, msk, d[x][y][msk] = u.d + 1});
}
}
puts("-1"); return 0;
}
试题库问题
将试题看作左部点,类型看作右部点,每个试题向对应的类型连边。源点向试题连边,类型向汇点连边。
有解仅当最大流为 ,方案的输出可以根据哪些边流满来判断。
查看代码
int k, n, m;
int main(void) {
scanf("%d%d", &k, &n); int T = n + k + 1;
for (int i = 1, v; i <= k; ++i) {
scanf("%d", &v); m += v;
g.addedge(n + i, T, v);
}
for (int i = 1; i <= n; ++i) {
g.addedge(0, i, 1);
int p, k; scanf("%d", &p);
while (p--) scanf("%d", &k), g.addedge(i, n + k, 1);
}
if (g.maxflow(0, T) != m) return puts("No Solution!"), 0;
for (int i = 1; i <= k; ++i) {
printf("%d: ", i);
for (int j = 0; j < g.G[n + i].size(); ++j) {
int it = g.edges[g.G[n + i][j]].v;
if (it <= n && g.edges[g.G[n + i][j]].w) printf("%d ", it);
}
putchar('\n');
}
return 0;
}
飞行员配对方案问题
二分图最大匹配模板。
查看代码
int n, m;
int main(void) {
scanf("%d%d", &m, &n);
for (int i = 1; i <= m; ++i) G.addedge(0, i, 1);
for (int i = m + 1; i <= n; ++i) G.addedge(i, n + 1, 1);
while (1) {
int x, y; cin >> x >> y;
if (x == -1) break;
G.addedge(x, y, 1);
}
printf("%d\n", G.maxFlow(0, n + 1));
G.print();
return 0;
}
圆桌问题
二分图最大多重匹配模板。
查看代码
int m, n, s;
int main(void) {
scanf("%d%d", &m, &n);
for (int i = 1, r; i <= m; ++i) scanf("%d", &r), G.addedge(0, i, r), s += r;
for (int i = 1, c; i <= n; ++i) scanf("%d", &c), G.addedge(m + i, n + m + 1, c);
for (int u = 1; u <= m; ++u)
for (int v = m + 1; v <= m + n; ++v)
G.addedge(u, v, 1);
if (G.maxFlow(0, n + m + 1) != s) return puts("0"), 0;
G.print(); return 0;
}
太空飞行计划问题
将仪器和实验抽象成点,就是最大权闭合子图模板。
查看代码
int m, n;
int main(void) {
cin >> m >> n; int t = m + n + 1, ans = 0;
for (int i = 1; i <= m; ++i) {
int w, x; cin >> w; G.addedge(0, i, w); ans += w;
string s; getline(cin, s); stringstream ss(s);
while (ss >> x) G.addedge(i, x + m, 1e9);
}
for (int i = 1, x; i <= n; ++i) cin >> x, G.addedge(i + m, t, x);
int flow = G.maxFlow(0, t);
for (int i = 1; i <= m; ++i) if (G.d[i] != -1) cout << i << ' '; cout << '\n';
for (int i = 1; i <= n; ++i) if (G.d[i + m] != -1) cout << i << ' ';
cout << '\n' << ans - flow << "\n";
return 0;
}
骑士共存问题
将棋盘上的点划分为左部点和右部点,使得骑士的位置和其能走到的位置在不同的点部。这样就成了二分图最大独立集的模板。
查看代码
const int DX[] = {-2, -2, -1, -1, 1, 1, 2, 2};
const int DY[] = {-1, 1, -2, 2, -2, 2, -1, 1};
int n, m;
bool a[205][205];
int f(int i, int j) { return (i - 1) * n + j; }
int main(void) {
cin >> n >> m;
for (int i = 1, u, v; i <= m; ++i) cin >> u >> v, a[u][v] = 1;
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j) if (!a[i][j]) {
if (i + j & 1) {
G.addedge(0, f(i, j), 1);
for (int k = 0; k < 8; ++k) {
int x = i + DX[k], y = j + DY[k];
if (x > 0 && y > 0 && x <= n && y <= n && !a[x][y]) G.addedge(f(i, j), f(x, y), 1);
}
}
else G.addedge(f(i, j), n * n + 1, 1);
}
cout << n * n - m - G.maxFlow(0, n * n + 1) << "\n";
return 0;
}
最长不下降子序列问题
通过 DP 来求解答案,拆点限制一个点的使用次数。
查看代码
int n;
int a[505], f[505];
int in(int x) { return x * 2 - 1; }
int out(int x) { return x * 2; }
int main(void) {
scanf("%d", &n);
for (int i = 1; i <= n; ++i) scanf("%d", a + i);
for (int i = 1; i <= n; ++i)
for (int j = 0; j < i; ++j)
if (a[i] >= a[j]) f[i] = max(f[i], f[j] + 1);
int ans = 0;
for (int i = 1; i <= n; ++i) ans = max(ans, f[i]);
printf("%d\n", ans);
if (ans == 1) return !printf("%d\n%d\n", n, n);
for (int i = 1; i <= n; ++i)
for (int j = 1; j < i; ++j)
if (a[i] >= a[j] && f[i] == f[j] + 1) G.addedge(out(j), in(i), 1);
for (int i = 1; i <= n; ++i) {
if (f[i] == 1) G.addedge(0, in(i), 1e9);
if (f[i] == ans) G.addedge(out(i), n * 2 + 1, 1e9);
} E = G;
for (int i = 1; i <= n; ++i) {
G.addedge(in(i), out(i), 1);
E.addedge(in(i), out(i), i == 1 || i == n ? n : 1);
}
printf("%d\n%d\n", G.maxFlow(0, n * 2 + 1), E.maxFlow(0, n * 2 + 1));
return 0;
}
最小路径覆盖问题
将点拆成左点和右点,进行二分图最大匹配。每次多流上一个,就说明又有一对点连在了一起。最后 DFS 输出答案即可。
查看代码
int in[155], to[155];
void dfs(int x) {
printf("%d ", x);
if (to[x]) dfs(to[x]);
}
int main(void) {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i) G.addedge(0, i, 1);
for (int i = 1; i <= n; ++i) G.addedge(i + n, n * 2 + 1, 1);
while (m--) {
int u, v; scanf("%d%d", &u, &v);
G.addedge(u, v + n, 1);
}
int ans = G.maxFlow(0, n * 2 + 1);
for (int u = 1; u <= n; ++u)
for (int i : G.G[u]) {
int v = G.edges[i].v, w = G.edges[i].w;
if (v > n && w == 0) in[to[u] = v - n] = 1;
}
for (int i = 1; i <= n; ++i) if (!in[i]) dfs(i), putchar('\n');
return !printf("%d\n", n - ans);
}
魔术球问题
枚举数,然后连可以放在一起的数的边,发现就是最小路径覆盖问题。
查看代码
int in[100005], to[100005];
void dfs(int x) {
printf("%d ", x);
if (to[x]) dfs(to[x]);
}
int main(void) {
scanf("%d", &n); int t = 100001;
for (int i = 1, flow = 0; ; ++i) {
G.addedge(0, i * 2 - 1, 1); G.addedge(i * 2, t, 1);
for (int j = 1; j < i; ++j) {
int d = sqrt(i + j);
if (d * d == i + j) G.addedge(j * 2 - 1, i * 2, 1);
}
flow += G.maxFlow(0, t);
if (i - flow > n) {
printf("%d\n", i - 1);
for (int u = 1; u < i; ++u)
for (int j : G.G[u * 2 - 1]) {
int v = G.edges[j].v / 2, w = G.edges[j].w;
if (v && v < i && w == 0) in[to[u] = v] = 1;
}
for (int j = 1; j < i; ++j) if (!in[j]) dfs(j), putchar('\n');
return 0;
}
}
return 0;
}
简单网络流
没有什么趣味性。
[USACO05NOV] Asteroids G
将行抽象成第一个条件,列抽象成第二个条件,就是二分图的最小点覆盖集。
查看代码
int n, k;
int main(void) {
scanf("%d%d", &n, &k);
for (int i = 1; i <= n; ++i) G.addedge(0, i, 1), G.addedge(i + n, n * 2 + 1, 1);
for (int i = 1; i <= k; ++i) {
int x, y; scanf("%d%d", &x, &y);
G.addedge(x, y + n, 1);
}
printf("%d\n", G.maxflow(0, n * 2 + 1));
return 0;
}
[THUPC2022 初赛] 分组作业
考虑集合划分模型。
唯一犯难的是这个喜欢关系,考虑将每个组也再建个点。如果组 在最终的残量网络上与 相连则表示合作,与 相连则表示不合作。任何一个人不同意这个组就不合作,因此组向它的组员连 的边,表示如果 与组这个点连通,说明合作,就只能断掉组员与 的连接。
将 向 对应的组连 的边,这样如果 同意了(它连向了 ), 组没有合作(连向了 ),则要割掉 。类似地, 的组向 连 。
查看代码
int n, m;
int main(void) {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n * 2; ++i) {
int c, d, e; scanf("%d%d%d", &c, &d, &e);
G.addedge(0, i, d); G.addedge(i, n * 3 + 1, c);
G.addedge(i, i % 2 == 0 ? i - 1 : i + 1, e);
G.addedge(n * 2 + (i + 1) / 2, i, 1e18);
}
for (int i = 1; i <= m; ++i) {
int A, B, a, b; scanf("%d%d%d%d", &A, &B, &a, &b);
G.addedge(B, n * 2 + (A + 1) / 2, a);
G.addedge(n * 2 + (B + 1) / 2, A, b);
}
printf("%lld\n", G.maxFlow(0, n * 3 + 1));
return 0;
}
[Luogu P1402] 酒店之王
其实是一个三分图匹配的结构,将人拆成两个点放在中间即可。
查看代码
int n, p, q;
int main(void) {
ios::sync_with_stdio(0);
cin >> n >> p >> q;
int t = n + p + q + n + 1;
for (int i = 1; i <= p; ++i) G.addedge(0, i, 1);
for (int i = 1; i <= n; ++i) G.addedge(p + i, p + n + i, 1);
for (int i = 1; i <= q; ++i) G.addedge(p + n * 2 + i, t, 1);
for (int i = 1, x; i <= n; ++i) for (int j = 1; j <= p; ++j) {
cin >> x;
if (x) G.addedge(j, p + i, 1);
}
for (int i = 1, x; i <= n; ++i) for (int j = 1; j <= q; ++j) {
cin >> x;
if (x) G.addedge(p + n + i, p + n * 2 + j, 1);
}
cout << G.maxFlow(0, t) << "\n";
return 0;
}
[Luogu P4313] 文理分科
考虑用全部价值减去最小的需要割掉的价值。前面两种是容易处理的,后面两种只需要各建立一个虚点,然后与一个点关联的虚点向这个点连 的边即可。
查看代码
int cal(int x, int y, int t) {
return (x - 1) * m + y + n * m * t;
}
int main(void) {
scanf("%d%d", &n, &m); int ans = 0, s = 0, t = n * m * 3 + 1;
for (int i = 1; i <= n; ++i) for (int j = 1; j <= m; ++j) {
scanf("%d", &a[i][j]); ans += a[i][j];
G.addedge(s, cal(i, j, 0), a[i][j]);
}
for (int i = 1; i <= n; ++i) for (int j = 1; j <= m; ++j) {
scanf("%d", &b[i][j]); ans += b[i][j];
G.addedge(cal(i, j, 0), t, b[i][j]);
}
for (int i = 1; i <= n; ++i) for (int j = 1; j <= m; ++j) {
scanf("%d", &c[i][j]); ans += c[i][j];
G.addedge(s, cal(i, j, 1), c[i][j]);
G.addedge(cal(i, j, 1), cal(i, j, 0), 1e9);
if (i > 1) G.addedge(cal(i - 1, j, 1), cal(i, j, 0), 1e9);
if (i < n) G.addedge(cal(i + 1, j, 1), cal(i, j, 0), 1e9);
if (j > 1) G.addedge(cal(i, j - 1, 1), cal(i, j, 0), 1e9);
if (j < m) G.addedge(cal(i, j + 1, 1), cal(i, j, 0), 1e9);
}
for (int i = 1; i <= n; ++i) for (int j = 1; j <= m; ++j) {
scanf("%d", &d[i][j]); ans += d[i][j];
G.addedge(cal(i, j, 2), t, d[i][j]);
G.addedge(cal(i, j, 0), cal(i, j, 2), 1e9);
if (i > 1) G.addedge(cal(i - 1, j, 0), cal(i, j, 2), 1e9);
if (i < n) G.addedge(cal(i + 1, j, 0), cal(i, j, 2), 1e9);
if (j > 1) G.addedge(cal(i, j - 1, 0), cal(i, j, 2), 1e9);
if (j < m) G.addedge(cal(i, j + 1, 0), cal(i, j, 2), 1e9);
}
return !printf("%d\n", ans - G.maxFlow(s, t));
}
二分图
一些题。
「Wdoi-6」最澄澈的空与海
必要条件是有一个节点入度为 ,从这个节点开始跑 bfs 看是否存在完美匹配。
查看代码
#include <bits/stdc++.h>
using namespace std;
int n, m, in[2000005];
bool del[2000005];
vector<int> G[2000005];
int main(void) {
int T; scanf("%d", &T);
while (T--) {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n * 2; ++i) G[i].clear(), in[i] = del[i] = 0;
while (m--) {
int u, v; scanf("%d%d", &u, &v); v += n; ++in[u]; ++in[v];
G[u].emplace_back(v); G[v].emplace_back(u);
}
queue<int> q; int cnt = 0;
for (int i = 1; i <= n * 2; ++i) if (in[i] == 1) q.push(i);
while (!q.empty()) {
int u = q.front(); q.pop();
if (del[u] || in[u] != 1) continue;
del[u] = 1; ++cnt; int buf = 0;
while (del[G[u][buf]]) ++buf;
del[G[u][buf]] = 1; ++cnt;
for (int v : G[G[u][buf]])
if (!del[v] && (--in[v]) == 1) q.push(v);
}
if (cnt == n * 2) puts("Renko"); else puts("Merry");
}
return 0;
}
[CF1139E] Maximize Mex
考虑静态的情况怎么做。将能力值定义为左部点,学生定义为右部点,初始令答案为 ,然后看 是否能够增广,能够增广就再看 。
倒着考虑这个过程,将删边改为加边即可。
查看代码
int n, m, q;
int a[5005], c[5005], d[5005], ans[5005];
bool v[5005];
int main(void) {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++i) G.addedge(n + i, n + m + 1, 1);
for (int i = 1; i <= n; ++i) scanf("%d", a + i);
for (int i = 1; i <= n; ++i) scanf("%d", c + i);
scanf("%d", &q);
for (int i = 1; i <= q; ++i) scanf("%d", d + i), v[d[i]] = 1;
for (int i = 1; i <= n; ++i) if (!v[i]) if (a[i] < n) G.addedge(a[i] + 1, n + c[i], 1);
G.addedge(0, 1, 1);
for (int i = q, cur = 0; i >= 1; --i) {
while (G.maxFlow(0, n + m + 1)) if (++cur < n) G.addedge(0, cur + 1, 1);
ans[i] = cur;
if (a[d[i]] < n) G.addedge(a[d[i]] + 1, n + c[d[i]], 1);
}
for (int i = 1; i <= q; ++i) printf("%d\n", ans[i]);
return 0;
}
[CF1592F] Alice and Recoloring
不难发现 操作是小丑,我们都可以使用 操作来代替。
先考虑 F1。将 W
看成 ,B
看成 ,然后考虑什么东西要被翻转。如果它自己是 需要翻吗?不一定! 是 需要翻,当且仅当 当中有 或 个是 。如果它自己不是 就不需要翻吗?也不是,同样是那三个位置,如果当中有 个 就需要翻。
令 ,只考虑 操作,答案就是 。
干了什么?相当于翻了 ,显然,它只能使用 次。代码。
再考虑 F2。我们考虑在刚才的基础上加一些限制。
首先,基于之前说过的 干的事情,我们只会操作 均为 的,否则不如来 操作。然后这样就发现了一个事情,一次操作 之后会将 和 废掉,也就是说,同行同列最多进行一次 操作。
这看上去很像一个二分图匹配问题了!将每行抽象为左部点,每列抽象为右部点(前提是 ),然后根据 进行连边。
查看代码
int n, m, a[505][505];
char s[505][505];
int main(void) {
ios::sync_with_stdio(0);
cin >> n >> m; int ans = 0;
for (int i = 1; i <= n; ++i) cin >> s[i] + 1;
for (int i = 1; i <= n; ++i) for (int j = 1; j <= m; ++j) a[i][j] = (s[i][j] != 'W');
for (int i = 1; i <= n; ++i) for (int j = 1; j <= m; ++j) a[i][j] ^= a[i + 1][j] ^ a[i][j + 1] ^ a[i + 1][j + 1];
for (int i = 1; i < n; ++i) for (int j = 1; j < m; ++j) if (a[i][j] && a[i][m] && a[n][j]) G.addedge(i, n + j, 1);
for (int i = 1; i < n; ++i) if (a[i][m]) G.addedge(0, i, 1);
for (int i = 1; i < m; ++i) if (a[n][i]) G.addedge(i + n, n + m + 1, 1);
int flow = G.maxFlow(0, n + m + 1); a[n][m] ^= (flow & 1);
for (int i = 1; i <= n; ++i) for (int j = 1; j <= m; ++j) ans += a[i][j];
return cout << ans - flow << "\n", 0;
}
综合应用
一些比较好玩的题。
[CF1728F] Fishermen
我们需要构造一个 且 互不相同,这样这个 就一定是满足条件的。
构造所有的 ,我们用这些数来匹配 。这样点数和边数都是 的。
考虑匈牙利算法,在找到匹配时清空 vis
数组,由于只有 个匹配,因此时间复杂度 。
查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long i64;
int n, m, a[1005], b[1000005];
int buc[1000005], mch[1000005];
bool vis[1000005];
vector<int> G[1000005];
bool dfs(int x) {
if (vis[x]) return 0; vis[x] = 1;
for (int y : G[x]) if (!mch[y] || dfs(mch[y])) return mch[y] = x, 1;
return 0;
}
int main(void) {
scanf("%d", &n);
for (int i = 1; i <= n; ++i) {
scanf("%d", a + i);
for (int j = 1; j <= n; ++j) b[++m] = a[i] * j;
} sort(b + 1, b + m + 1);
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j) {
int p = lower_bound(b + 1, b + m + 1, a[i] * j) - b;
G[p].emplace_back(i);
}
i64 ans = 0;
for (int i = 1; i <= m; ++i) if (dfs(i)) ans += b[i], memset(vis, 0, sizeof vis);
return !printf("%lld\n", ans);
}
[WC2007] 剪刀石头布
如果一个三元组不是三元环,那么有一个点的出度为 。假定一个点的出度为 ,那么它会使得三元环减少 ,那么最终答案为:
考虑最小费用最大流。每个人向汇点连容量为 的边,源点向每个不确定的边连边、边向两个人连边,容量均为 。
一个人的贡献可以看成 ,这是等差数列的费用。套路地,考虑拆点,拆成 个,费用依次为 即可。代码。
虽然在《算法导论》中并没有这个定义,但是这样可以简单的支持反悔。 ↩︎