「刷题记录」网络流24题
有标题栏,可以直接通过标题栏跳转
1、「网络流 24 题」搭配飞行员
这是最简单的一道题了,题中说一个正驾驶员只能搭配一个副驾驶员,由此,我们可以转化成二分图,这道题就变成了求二分图最大匹配,匈牙利或 Dinic 算法都可以
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
inline ll read() {
ll x = 0;
int fg = 0;
char ch = getchar();
while (ch < '0' || ch > '9') {
fg |= (ch == '-');
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return fg ? ~x + 1 : x;
}
const int N = 1e4 + 5;
int n, m, S, T, cnt;
ll maxflow;
int h[N], dep[N], cur[N];
bool inque[N];
struct edge {
int v, nxt;
ll w;
} e[N << 2];
inline void add(int u, int v, ll w) {
e[++ cnt].v = v;
e[cnt].nxt = h[u];
e[cnt].w = w;
h[u] = cnt;
}
inline bool bfs() {
queue<int> q;
for (int i = 0; i <= T; ++ i) {
dep[i] = 1e9;
inque[i] = 0;
cur[i] = h[i];
}
q.push(S);
dep[S] = 0, inque[S] = 1;
while (!q.empty()) {
int u = q.front();
q.pop();
inque[u] = 0;
for (int i = h[u]; i; i = e[i].nxt) {
int v = e[i].v;
if (e[i].w && dep[v] > dep[u] + 1) {
dep[v] = dep[u] + 1;
if (!inque[v]) {
q.push(v);
inque[v] = 1;
}
if (v == T) return 1;
}
}
}
return 0;
}
inline ll dfs(int u, ll flow) {
if (u == T) {
maxflow += flow;
return flow;
}
ll used = 0, rlow = 0;
for (int i = cur[u]; i; i = e[i].nxt) {
cur[u] = i;
int v = e[i].v;
if (e[i].w && dep[v] == dep[u] + 1) {
rlow = dfs(v, min(flow - used, e[i].w));
if (rlow) {
used += rlow;
e[i].w -= rlow;
e[i ^ 1].w += rlow;
if (used == flow) break;
}
}
}
return used;
}
int main() {
n = read(), m = read();
T = n + 1, cnt = 1;
int u, v;
while (~(scanf("%d%d", &u, &v))) {
add(u, v, 1);
add(v, u, 0);
}
for (int i = 1; i <= m; ++ i) {
add(S, i, 1);
add(i, S, 0);
}
for (int i = m + 1; i <= n; ++ i) {
add(i, T, 1);
add(T, i, 0);
}
while (bfs()) {
dfs(S, 1e18);
}
printf("%lld\n", maxflow);
return 0;
}
2、「网络流 24 题」太空飞行计划
上来这个题感觉不是很好理解,但代码和建图是真简单
题中说,要使净收入最大,净收入 \(=\) 参与的实验最后全部的收入 \(-\) 安装仪器的费用,我们想,如果我们的最后的收入比最后的安装费用小,那我们就没有必要进行实验(你还赔了钱),如果我们最后的收入比最后的安装费用大,那我们就有净收入
我们先建图,建一个超级源点连向所有的实验,边权为支付的费用,建一个超级汇点连向所有仪器,边权为仪器安装费用,最后流入汇点的是我们进行实验花的钱,这里就用到了最小割,如果安装费用比收入大,那割的是进行实验的边,否则,割的是安装仪器的边,如果收入大于支出(安装费用),那我们的净收入就是总的收入 \(-\) 最小割,否则,净收入就是总的收入 \(-\) 最小割(总的收入)\(= 0\)
这里涉及一个模型:最大权闭合子图
这里还设计一个定理:最大流等于最小割
简单证明一下
最大流的值取决于每条流径中流量最小的那一条边,即最大流相当于是每条流径中流量最小的边的和,而最小割就是求切断后能使图分为两半的边的最小流量,也就相当于每条流径中流量最小的边的和,所以最大流等于最小割
如何保证最小割的答案来源于最大流?如果已经求出最大流了,那么在图中就再也找不到能从源点流向汇点的路径了(否则就是你最大流求错了),所以可以保证最小割的答案来源都在最大流的流径上
最大权闭合子图,就是说你选了一个点 \(u\),那么和 \(u\) 有关系的点(相连的点)都要选,而且要选到底
具体可以参考这边博客最大权闭合子图
代码很好写
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
inline ll read() {
ll x = 0;
int fg = 0;
char ch = getchar();
while (ch < '0' || ch > '9') {
fg |= (ch == '-');
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return fg ? ~x + 1 : x;
}
const int N = 210;
int n, m, cnt, S, T;
ll sum, maxflow;
int h[N], dep[N], cur[N];
bool inque[N];
struct edge {
int v, nxt;
ll w;
} e[N * N << 1];
inline void add(int u, int v, ll w) {
e[++ cnt].v = v;
e[cnt].w = w;
e[cnt].nxt = h[u];
h[u] = cnt;
}
inline bool bfs() {
queue<int> q;
for (int i = S; i <= T; ++ i) {
dep[i] = 1e9;
cur[i] = h[i];
inque[i] = 0;
}
q.push(S);
dep[S] = 0;
while (!q.empty()) {
int u = q.front();
q.pop();
inque[u] = 0;
for (int i = h[u]; i; i = e[i].nxt) {
int v = e[i].v;
if (e[i].w && dep[v] > dep[u] + 1) {
dep[v] = dep[u] + 1;
if (!inque[v]) {
q.push(v);
inque[v] = 1;
}
if (v == T) return 1;
}
}
}
return 0;
}
inline ll dfs(int u, ll flow) {
if (u == T) {
maxflow += flow;
return flow;
}
ll used = 0, rlow = 0;
for (int i = cur[u]; i; i = e[i].nxt) {
cur[u] = i;
int v = e[i].v;
if (dep[v] == dep[u] + 1 && e[i].w) {
rlow = dfs(v, min(e[i].w, flow - used));
if (rlow) {
used += rlow;
e[i].w -= rlow;
e[i ^ 1].w += rlow;
if (used == flow) break;
}
}
}
if (!used) dep[u] = 1e9;
return used;
}
int main() {
m = read(), n = read();
cnt = 1;
T = n + m + 1;
for (int i = 1; i <= m; ++ i) {
ll x = read();
char ch;
sum += x;
add(S, i, x), add(i, S, 0);
while (scanf("%lld%c", &x, &ch)) {
add(i, x + m, 1e18);
add(x + m, i, 0);
if (ch == '\n' || ch == '\r') break;
}
}
for (int i = 1; i <= n; ++ i) {
ll val = read();
add(i + m, T, val);
add(T, i + m, 0);
}
while (bfs()) {
dfs(S, 1e18);
}
for (int i = 1; i <= m; ++ i) {
if (dep[i] != 1e9) {
printf("%d ", i);
}
}
putchar('\n');
for (int i = 1; i <= n; ++ i) {
if (dep[i + m] != 1e9) {
printf("%d ", i);
}
}
putchar('\n');
printf("%lld\n", sum - maxflow);
return 0;
}
3、「网络流 24 题」最小路径覆盖
题意很明确,找到最少的不相交的路径,是路径上的点覆盖这个图上所有的点,这里要注意,单独一个点也算是一条路径
建图:我们要进行一个操作——拆点,将一个点 \(i\) 拆成两个 \((i, i + n)\),前者代表别的点流向这个点,后者代表这个点出发流向别的点,由于单独一个点也算是一天路径,所以源点要向所有点 \(i\) 连一条边,权值为 \(1\),所有点再向汇点连一条边,权值也为 \(1\),再根据题目中所说的边的关系建边 \((u + n \rightarrow v)\),跑最大流即可,这个也相当于二分图最大匹配
这里找到最大流后,我们再回到题中,在最开始,我们可以把图中的每一个点都看作是一条路径,这样就有 \(n\) 条路经,每当有两个点连边(被匹配),相当于将两个路径合并,最后路径个数就是 ($n - $最大匹配数),输出路径我用的并查集,注意并查集的父亲的方向
最小路径覆盖可以看看这篇文章:最小路径覆盖
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
inline ll read() {
ll x = 0;
int fg = 0;
char ch = getchar();
while (ch < '0' || ch > '9') {
fg |= (ch == '-');
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return fg ? ~x + 1 : x;
}
const int N = 210;
const int M = 6e3 + 5;
int n, m, cnt = 1, S, T;
ll maxflow, ans;
int h[N << 1], dep[N << 1], cur[N << 1], f[N << 1];
bool inque[N << 1];
struct edge {
int v, nxt;
ll w;
} e[M << 2];
inline void add(int u, int v, ll w) {
e[++ cnt].v = v;
e[cnt].nxt = h[u];
h[u] = cnt;
e[cnt].w = w;
}
bool bfs() {
queue<int> q;
for (int i = S; i <= T; ++ i) {
dep[i] = 1e9;
inque[i] = 0;
cur[i] = h[i];
}
q.push(S);
dep[S] = 0, inque[S] = 1;
while (!q.empty()) {
int u = q.front();
q.pop();
inque[u] = 0;
for (int i = h[u]; i; i = e[i].nxt) {
int v = e[i].v;
if (e[i].w && dep[v] > dep[u] + 1) {
dep[v] = dep[u] + 1;
if (!inque[v]) {
q.push(v);
inque[v] = 1;
}
if (v == T) return 1;
}
}
}
return 0;
}
ll dfs(int u, ll flow) {
if (u == T) {
maxflow += flow;
return flow;
}
ll used = 0, rlow = 0;
for (int i = cur[u]; i; i = e[i].nxt) {
cur[u] = i;
int v = e[i].v;
if (dep[v] == dep[u] + 1 && e[i].w) {
rlow = dfs(v, min(flow - used, e[i].w));
if (rlow) {
used += rlow;
e[i].w -= rlow;
e[i ^ 1].w += rlow;
if (used == flow) break;
}
}
}
if (!used) dep[u] = 1e9;
return used;
}
inline int find(int x) {
return f[x] == x ? f[x] : f[x] = find(f[x]);
}
inline void Union(int a, int b) {
int fa = find(a), fb = find(b);
if (fa != fb) f[fa] = fb;
}
int main() {
n = read(), m = read();
cnt = 1;
T = n + n + 1;
for (int i = 1; i <= m; ++ i) {
int u = read(), v = read();
add(u, v + n, 1);
add(v + n, u, 0);
}
for (int i = 1; i <= n; ++ i) {
add(S, i, 1);
add(i, S, 0);
add(i + n, T, 1);
add(T, i + n, 0);
}
while (bfs()) {
dfs(S, 1e18);
}
ans = n - maxflow;
for (int i = 1; i <= n; ++ i) {
f[i] = i;
}
for (int u = 1; u <= n; ++ u) {
for (int i = h[u]; i; i = e[i].nxt) {
if (!e[i].w && e[i].v - n <= n && e[i].v) {
Union(u, e[i].v - n);
}
}
}
for (int i = n; i; -- i) {
bool fg = 0;
for (int j = 1; j <= n; ++ j) {
if (find(j) == i) {
printf("%d ", j);
fg = 1;
}
}
if (fg) {
putchar('\n');
}
}
printf("%lld\n", ans);
return 0;
}
4、「网络流 24 题」魔术球
这个和上一个题差不多,只不过这个题的建边条件有点不同,如果两个数的和为完全平方数才能建边,每个点后面只能跟一个点,即都是简单路径,如果新节点没法在任何节点后面接上,那就放到新柱子上,即开一条新的路径,由于柱子数为 \(n\),每开一个新路径要用一个柱子,所以当路径数大于 \(n\) 时,代表这个点(当前操作的点)已经放不进去了,在它前面的点就是答案,路径输出还是并查集
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
inline ll read() {
ll x = 0;
int fg = 0;
char ch = getchar();
while (ch < '0' || ch > '9') {
fg |= (ch == '-');
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return fg ? ~x + 1 : x;
}
const int N = 1e4 + 5;
int n, cnt, T, S;
ll maxflow;
int h[(N << 1) + 2], dep[(N << 1) + 2], cur[(N << 1) + 2], fa[(N << 1) + 2];
bool inque[(N << 1) + 2];
unordered_map<ll, int> mp;
struct edge {
int v, nxt;
ll w;
} e[N * 100];
inline void add(int u, int v, ll w) {
e[++ cnt].v = v;
e[cnt].w = w;
e[cnt].nxt = h[u];
h[u] = cnt;
}
bool bfs() {
queue<int> q;
for (int i = S; i <= T; ++ i) {
cur[i] = h[i];
inque[i] = 0;
dep[i] = 1e9;
}
q.push(S);
dep[S] = 0;
while (!q.empty()) {
int u = q.front();
q.pop();
inque[u] = 0;
for (int i = h[u]; i; i = e[i].nxt) {
int v = e[i].v;
if (e[i].w && dep[v] > dep[u] + 1) {
dep[v] = dep[u] + 1;
if (!inque[v]) {
q.push(v);
inque[v] = 1;
}
if (v == T) return 1;
}
}
}
return 0;
}
ll dfs(int u, ll flow) {
if (u == T) {
maxflow += flow;
return flow;
}
ll used = 0, rlow = 0;
for (int i = cur[u]; i; i = e[i].nxt) {
cur[u] = i;
int v = e[i].v;
if (dep[v] == dep[u] + 1 && e[i].w) {
rlow = dfs(v, min(flow - used, e[i].w));
if (rlow) {
used += rlow;
e[i].w -= rlow;
e[i ^ 1].w += rlow;
if (used == flow) break;
}
}
}
return used;
}
int find(int x) {
return fa[x] == x ? fa[x] : fa[x] = find(fa[x]);
}
inline void Union(int a, int b) {
int fx = find(a), fy = find(b);
if (fx != fy) fa[fx] = fy;
}
int main() {
int ans = 0, num = 0;
n = read();
cnt = 1;
T = N + N + 1;
for (int i = 1; i <= 1000; ++ i) {
mp[i * i] = 1;
}
for (int i = 1; i; ++ i) {
++ num;
add(S, i, 1), add(i, S, 0);
add(i + N, T, 1), add(T, i + N, 0);
for (int j = 1; j < i; ++ j) {
if (mp.find(i + j) != mp.end()) {
add(i, j + N, 1);
add(j + N, i, 0);
}
}
while (bfs()) {
dfs(S, 1e18);
}
if (num - maxflow > n) break;
++ ans;
}
printf("%d\n", ans);
for (int i = 1; i <= ans; ++ i) {
fa[i] = i;
}
for (int u = 1; u <= ans; ++ u) {
for (int i = h[u]; i; i = e[i].nxt) {
int v = e[i].v;
if (!e[i].w && v - N <= ans && v) {
Union(min(u, v - N), max(u, v - N));
}
}
}
for (int i = 1; i <= ans; ++ i) {
int fg = 0;
for (int j = 1; j <= i; ++ j) {
if (find(j) == i) {
fg = 1;
printf("%d ", j);
}
}
if (fg) putchar('\n');
}
return 0;
}
5、「网络流 24 题」圆桌聚餐
这个题其实就很简单了,根据 套路 题意,单位企业肯定要和圆桌连边,题中说每个桌子坐一个单位的代表,所以边权为 \(1\),我们让源点与每一个单位连边,边权为单位的人数,将圆桌与汇点连边,边权为圆桌能容纳的人数,跑最大流即可,这里要注意,我们要求的合法方案是每个人都能按要求(每个单位在每个桌子上只能有一个人)坐下,如果最后返回的最大流不等于总人数,就是无解。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
inline ll read() {
ll x = 0;
int fg = 0;
char ch = getchar();
while (ch < '0' || ch > '9') {
fg |= (ch == '-');
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return fg ? ~x + 1 : x;
}
const int M = 155;
const int N = 305;
int m, n, S, T, cnt = 1;
ll maxflow, sum;
int h[N + M], cur[N + M], dep[N + M];
bool inque[N + M];
struct edge {
int v, nxt;
ll w;
} e[N * M << 2];
inline void add(int u, int v, ll w) {
e[++ cnt].v = v;
e[cnt].nxt = h[u];
h[u] = cnt;
e[cnt].w = w;
}
bool bfs() {
queue<int> q;
for (int i = S; i <= T; ++ i) {
cur[i] = h[i];
dep[i] = 1e9;
inque[i] = 0;
}
q.push(S);
inque[S] = 0, dep[S] = 0;
while (!q.empty()) {
int u = q.front();
q.pop();
inque[u] = 0;
for (int i = h[u]; i; i = e[i].nxt) {
int v = e[i].v;
if (e[i].w && dep[v] > dep[u] + 1) {
dep[v] = dep[u] + 1;
if (!inque[v]) {
q.push(v);
inque[v] = 1;
}
if (v == T) return 1;
}
}
}
return 0;
}
ll dfs(int u, ll flow) {
if (u == T) {
maxflow += flow;
return flow;
}
ll used = 0, rlow = 0;
for (int i = cur[u]; i; i = e[i].nxt) {
cur[u] = i;
int v = e[i].v;
if (e[i].w && dep[v] == dep[u] + 1) {
rlow = dfs(v, min(flow - used, e[i].w));
if (rlow) {
used += rlow;
e[i].w -= rlow;
e[i ^ 1].w += rlow;
if (used == flow) break;
}
}
}
return used;
}
int main() {
m = read(), n = read();
T = n + m + 1;
for (int i = 1; i <= m; ++ i) {
ll num = read();
sum += num;
add(S, i, num);
add(i, S, 0);
}
for (int i = 1; i <= n; ++ i) {
ll hid = read();
add(i + m, T, hid);
add(T, i + m, 0);
}
for (int i = 1; i <= m; ++ i) {
for (int j = 1; j <= n; ++ j) {
add(i, j + m, 1);
add(j + m, i, 0);
}
}
while (bfs()) {
dfs(S, 1e18);
}
if (maxflow == sum) printf("1\n");
else {
printf("0\n");
return 0;
}
for (int u = 1; u <= m; ++ u) {
for (int i = h[u]; i; i = e[i].nxt) {
int v = e[i].v;
if (!e[i].w && v > m && v <= m + n) {
printf("%d ", v - m);
}
}
putchar('\n');
}
return 0;
}
6、「网络流 24 题」最长递增子序列
这个题。。。怎么说,考的很全面 甚至考到了 DP
第一问就按照 \(\text{DP}\) 做就行了,最长不下降子序列,记录下值,后面要用(值的含义:以这个数字结尾的最长不下降子序列的长度)
第二问,条件是每个数字只能用一次,由于我们不确定最长不下降子序列的起始位置,所以源点要向每个位置 \(i\) 连边,边权为 \(1\),同时又涉及连接前后,所以我们还要拆点,如果一个位置的 DP 值是我们第一问求出的最大值,那么我们就将这个位置 \(i + n\) 向汇点连一条边(毕竟没有更长的了,你也只是统计个数),边权为 \(1\)
现在,源点汇点都连好了,就剩下每个位置之间的连边了,首先,每个点只能用一次,所以我们拆成的两个点之间要连一条边权为 \(1\) 的边,只有流经这条边,才代表用了这个数字,接下来,从前面扫到当前位置,由在最长不下降子序列中可能是这个数的前一个数向这个数连边(说人话就是,他可能排在你前面,那他就向你连一条边,没准你俩能成),判断就是这个前面这个数小于等于当前这个数,并且 \(DP_i + 1 = DP_j\),\(i\) 表示前面的数,\(j\) 表示当前的数,连好后跑最大流
第三问,就是将 \(1 \rightarrow i + n\)、\(n \rightarrow n + n\)、\(S \rightarrow 1\) 的流量改为无穷大,我们这里还要特判一下,如果这个数列中只有两个数,那么我们就没必要将 \(n \rightarrow n + n\) 的边权设为 \(1\) 了(毕竟再怎么选也只有这两个点,都是一样的),同时还要保证 \(DP_{n + n} =\) 第一问的值(毕竟如果以这个点为结尾没有最长的子序列,那连这个点有何用,增加错误答案?)
再不明白看看代码吧
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
inline ll read() {
ll x = 0;
int fg = 0;
char ch = getchar();
while (ch < '0' || ch > '9') {
fg |= (ch == '-');
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return fg ? ~x + 1 : x;
}
const int N = 510;
int n, l, cnt = 1, S, T;
ll maxflow;
int s[N], dp[N], h[N << 1], cur[N << 1], dep[N << 1];
bool inque[N << 1];
vector<int> vec;
struct edge {
int v, nxt;
ll w;
} e[N * N << 1];
inline void add(int u, int v, ll w) {
e[++ cnt].v = v;
e[cnt].w = w;
e[cnt].nxt = h[u];
h[u] = cnt;
}
bool bfs() {
queue<int> q;
for (int i = S; i <= T; ++ i) {
dep[i] = 1e9;
inque[i] = 0;
cur[i] = h[i];
}
q.push(S);
dep[S] = 0;
while (!q.empty()) {
int u = q.front();
q.pop();
inque[u] = 0;
for (int i = h[u]; i; i = e[i].nxt) {
int v = e[i].v;
if (e[i].w && dep[v] > dep[u] + 1) {
dep[v] = dep[u] + 1;
if (!inque[v]) {
q.push(v);
inque[v] = 1;
}
if (v == T) return 1;
}
}
}
return 0;
}
ll dfs(int u, ll flow) {
if (u == T) {
maxflow += flow;
return flow;
}
ll used = 0, rlow = 0;
for (int i = cur[u]; i; i = e[i].nxt) {
cur[u] = i;
int v = e[i].v;
if (e[i].w && dep[v] == dep[u] + 1) {
rlow = dfs(v, min(flow - used, e[i].w));
if (rlow) {
used += rlow;
e[i].w -= rlow;
e[i ^ 1].w += rlow;
if (used == flow) break;
}
}
}
return used;
}
int main() {
n = read();
T = n + n + 1;
cnt = 1;
for (int i = 1; i <= n; ++ i) {
s[i] = read();
dp[i] = 1;
}
for (int i = 1; i <= n; ++ i) {
if (vec.empty() || s[i] >= vec.back()) {
vec.push_back(s[i]);
dp[i] = (int)vec.size();
}
else {
int pos = upper_bound(vec.begin(), vec.end(), s[i]) - vec.begin(); // 二分
vec[pos] = s[i];
dp[i] = pos + 1;
}
l = max(l, dp[i]);
}
printf("%d\n", l);
for (int i = 1; i <= n; ++ i) {
if (dp[i] == 1) {
add(S, i, 1);
add(i, S, 0);
}
if (dp[i] == l) {
add(i + n, T, 1);
add(T, i + n, 0);
}
add(i, i + n, 1);
add(i + n, i, 0);
}
for (int i = 1; i <= n; ++ i) {
for (int j = 1; j < i; ++ j) {
if (s[j] <= s[i] && dp[j] + 1 == dp[i]) { // 找前一个位置
add(j + n, i, 1);
add(i, j + n, 0);
}
}
}
while (bfs()) {
dfs(S, 1e18);
}
printf("%lld\n", maxflow);
add(1, 1 + n, 1e18);
add(1 + n, 1, 0);
add(S, 1, 1e18);
add(1, S, 0);
add(n, n + n, 1e18);
add(n + n, n, 0);
if (dp[n] == l && n != 1) { // 特判
add(n + n, T, 1e18);
add(T, n + n, 0);
}
while (bfs()) {
dfs(S, 1e18);
}
printf("%lld\n", maxflow);
return 0;
}
7、「网络流 24 题」试题库
这道题和上面的圆桌问题很想,建一个超级源点,向每一个试题类型连边,边权为这个类型的题要出的题数,每道试题(注意,是题,不是前面的类型)都向汇点连一条边,边权为 \(1\),流经了就说明选了这个题,每个试卷类型再向各自对应的题目连边,边权为 \(1\),然后跑最大流,如果最后最大流不等于 \(m\),说明无解,有解的话,我们通过跑残量图来输出答案,如果选了这个题,那么这条边的反向边的流量一定为 \(1\)(反正不为 \(0\))
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
inline ll read() {
ll x = 0;
int fg = 0;
char ch = getchar();
while (ch < '0' || ch > '9') {
fg |= (ch == '-');
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return fg ? ~x + 1 : x;
}
const int N = 1050;
int k, n, S, T, cnt = 1;
ll maxflow, sum;
int h[N], dep[N], cur[N];
bool inque[N];
struct edge {
int v, nxt;
ll w;
} e[N * N << 1];
inline void add(int u, int v, ll w) {
e[++ cnt].v = v;
e[cnt].w = w;
e[cnt].nxt = h[u];
h[u] = cnt;
}
bool bfs() {
queue<int> q;
for (int i = S; i <= T; ++ i) {
dep[i] = 1e9;
cur[i] = h[i];
inque[i] = 0;
}
q.push(S);
dep[S] = 0;
while (!q.empty()) {
int u = q.front();
q.pop();
for (int i = h[u]; i; i = e[i].nxt) {
int v = e[i].v;
inque[u] = 0;
if (e[i].w && dep[v] > dep[u] + 1) {
dep[v] = dep[u] + 1;
if (!inque[v]) {
q.push(v);
inque[v] = 1;
}
if (v == T) return 1;
}
}
}
return 0;
}
ll dfs(int u, ll flow) {
if (u == T) {
maxflow += flow;
return flow;
}
ll used = 0, rlow = 0;
for (int i = cur[u]; i; i = e[i].nxt) {
cur[u] = i;
int v = e[i].v;
if (e[i].w && dep[v] == dep[u] + 1) {
rlow = dfs(v, min(flow - used, e[i].w));
if (rlow) {
used += rlow;
e[i].w -= rlow;
e[i ^ 1].w += rlow;
if (used == flow) break;
}
}
}
return used;
}
int main() {
k = read(), n = read();
T = k + n + 1;
cnt = 1;
for (int i = 1; i <= k; ++ i) {
ll x = read();
sum += x;
add(S, i, x);
add(i, S, 0);
}
for (int i = 1; i <= n; ++ i) {
int p = read(), x;
for (int j = 1; j <= p; ++ j) {
x = read();
add(x, i + k, 1);
add(i + k, x, 0);
}
add(i + k, T, 1);
add(T, i + k, 0);
}
while (bfs()) {
dfs(S, 1e18);
}
if (maxflow != sum) {
puts("No Solution!");
return 0;
}
for (int i = 1; i <= k; ++ i) {
printf("%d: ", i);
for (int j = h[i]; j; j = e[j].nxt) {
if (!e[j].w && e[j].v) {
printf("%d ", e[j].v - k);
}
}
putchar('\n');
}
return 0;
}
8、「网络流 24 题」方格取数
题目给我们的信息是:你选了一个格,那么这个格的上下左右你都不能选,我们要求最大的和,那么我们也可以使不选的方格的数的和最小来得到最大答案(这是容斥吗?蒟蒻发言),我们来想一下,如果我们将格子里的数转移到边权上,像不像最小割?割断一条边,代表这两个数不会同时选
同时,我们会发现,你选了一个格,那与这个格子同对角线的格子不会受这个格子影响,对吧?在扩展一些,假设这个格子行数加列数是一个奇数,那么所有行数加列数都为奇数的格子都不会受影响,这些格子一定可以同时选,行数加列数为偶数的有的能选,有的不能选
现在,说一下这个题的建图方法,我们将所有格子分为两类,一类是行数加列数为奇数的格子,一类是行数加列数为偶数的格子(每一类中的格子之间可以同时选),将不能同时选的格子连上边,边权为 \(+ \infty\),因为最小割等于最大流,最大流的值取决于每条流径中流量最小的那一条边,而最小割就是求切断后能使图分为两半的边的最小流量,所以设为 \(+ \infty\) 不会影响最小割的取值,也不会割断这一条边,源点向奇数类格子连边,边权为这个格子的权值,偶数类的格子向汇点连边,边权为格子的取值,(如果割断了源点到奇数类的边,说明没选那个奇数类的点;如果割断的是偶数类到汇点的边,说明没选那个偶数类的点)最后用答案减去最大流
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
inline ll read() {
ll x = 0;
int fg = 0;
char ch = getchar();
while (ch < '0' || ch > '9') {
fg |= (ch == '-');
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return fg ? ~x + 1 : x;
}
const int N = 110;
const int M = ((N * N) << 2);
int m, n, S, T, cnt, cot = 1;
ll maxflow, sum;
int g[N][N], id[N][N], h[N * N], dep[N * N], cur[N * N];
int inque[N * N];
struct edge {
int v, nxt;
ll w;
} e[M];
inline void add(int u, int v, ll w) {
e[++ cot].v = v;
e[cot].w = w;
e[cot].nxt = h[u];
h[u] = cot;
}
bool bfs() {
queue<int> q;
for (int i = S; i <= T; ++ i) {
inque[i] = 0;
dep[i] = 1e9;
cur[i] = h[i];
}
q.push(S);
dep[S] = 0;
while (!q.empty()) {
int u = q.front();
q.pop();
inque[u] = 0;
for (int i = h[u]; i; i = e[i].nxt) {
int v = e[i].v;
if (e[i].w && dep[v] > dep[u] + 1) {
dep[v] = dep[u] + 1;
if (!inque[v]) {
q.push(v);
inque[v] = 1;
}
if (v == T) return 1;
}
}
}
return 0;
}
ll dfs(int u, ll flow) {
if (u == T) {
maxflow += flow;
return flow;
}
ll used = 0, rlow = 0;
for (int i = cur[u]; i; i = e[i].nxt) {
cur[u] = i;
int v = e[i].v;
if (e[i].w && dep[v] == dep[u] + 1) {
rlow = dfs(v, min(flow - used, e[i].w));
if (rlow) {
used += rlow;
e[i].w -= rlow;
e[i ^ 1].w += rlow;
if (used == flow) break;
}
}
}
return used;
}
int main() {
m = read(), n = read();
int cj = n * m;
T = cj + 1;
for (int i = 1; i <= m; ++ i) {
for (int j = 1; j <= n; ++ j) {
g[i][j] = read();
id[i][j] = ++ cnt;
sum += g[i][j];
if ((i + j) & 1) {
add(S, id[i][j], g[i][j]);
add(id[i][j], S, 0);
}
else {
add(id[i][j], T, g[i][j]);
add(T, id[i][j], 0);
}
}
}
for (int i = 1; i <= m; ++ i) {
for (int j = 1; j <= n; ++ j) {
if (!((i + j) & 1)) continue;
if (i + 1 <= m) {
add(id[i][j], id[i + 1][j], 1e18);
add(id[i + 1][j], id[i][j], 0);
}
if (j + 1 <= n) {
add(id[i][j], id[i][j + 1], 1e18);
add(id[i][j + 1], id[i][j], 0);
}
if (i - 1 > 0) {
add(id[i][j], id[i - 1][j], 1e18);
add(id[i - 1][j], id[i][j], 0);
}
if (j - 1 > 0) {
add(id[i][j], id[i][j - 1], 1e18);
add(id[i][j - 1], id[i][j], 0);
}
}
}
while (bfs()) {
dfs(S, 1e18);
}
printf("%lld\n", sum - maxflow);
return 0;
}
9、「网络流 24 题」餐巾计划
这道题,费用流的开始
这道题的关系有点复杂,慢慢来,对于每一天,有开始和结束两个时刻,开始时要有足够的干净的餐巾,结束时会有脏的餐巾,对于脏餐巾,可以拖着不洗,也可以送到快洗店或慢洗店,在一定天数后得到干净的餐巾,对于干净的餐巾,可以新买(毕竟你第一天没餐巾,你只能买),也可以提前送到快慢洗店去洗,然后在用的那一天取回来
现在关系整清了,如何建图呢?一天有两个时间点,那我们将一天拆成两个点 \((i, i + n)\),前者为开始,后者为结束,如果拖着,那就将 \(i + n \rightarrow i + n + 1\) 连边,费用为零(毕竟不用洗),如果送到快洗店,要洗 \(t_1\) 天,那么将 \(i + n \rightarrow i + t_1\) 连边,费用为快洗店费用,慢洗店同理,慢洗店要洗 \(t_2\) 天,那么将 \(i + n \rightarrow i + t_2\) 连边,费用为慢洗店费用,以上边的流量都为 \(+ \infty\)(题目中没说洗衣店有数量限制),接下来,就是源点与汇点的关系了,源点代表的含义是提供餐巾(可能是干净的,也可能是脏的),汇点代表的含义是收集餐巾,所以,源点向结束点连边(源点向结束点提供脏餐巾),流量为这一天的餐巾数需要,开始点要向汇点连边(开始点向汇点提供脏餐巾),流量也为这一天的餐巾需要,这两个边费用都为 \(0\),同时原点还要向开始点连边,费用为 购买餐巾的费用(源点向开始点提供干净的餐巾源点区别对待),流量为 \(+ \infty\)
图联通了,跑最小费用最大流
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
inline ll read() {
ll x = 0;
int fg = 0;
char ch = getchar();
while (ch < '0' || ch > '9') {
fg |= (ch == '-');
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return fg ? ~x + 1 : x;
}
const int N = 2010;
const ll inf = 1e18;
int n, cnt = 1, S, T, t1, t2;
ll maxflow, cost, p, a, b;
int h[N << 1], d[N << 1];
ll r[N];
bool inque[N << 1];
struct edge {
int v, nxt;
ll w, f;
} e[300010];
inline void add(int u, int v, ll w, ll f) {
e[++ cnt].v = v;
e[cnt].nxt = h[u];
h[u] = cnt;
e[cnt].w = w;
e[cnt].f = f;
}
bool spfa() {
for (int i = 0; i <= T; ++ i) {
inque[i] = 0;
d[i] = 1e9;
}
deque<int> q;
q.push_back(S);
inque[S] = 1;
d[S] = 0;
while (!q.empty()) {
int u = q.front();
q.pop_front();
inque[u] = 0;
for (int i = h[u]; i; i = e[i].nxt) {
int v = e[i].v;
if (e[i].w && d[v] > d[u] + e[i].f) {
d[v] = d[u] + e[i].f;
if (!inque[v]) {
inque[v] = 1;
if (!q.empty() && d[v] <= d[q.front()]) {
q.push_front(v);
}
else {
q.push_back(v);
}
}
}
}
}
return (d[T] < 1e9);
}
ll dfs(int u, ll flow) {
if (u == T) {
maxflow += flow;
return flow;
}
ll rest = flow, rlow = 0;
inque[u] = 1;
for (int i = h[u]; i && rest; i = e[i].nxt) {
int v = e[i].v;
if (e[i].w <= 0 || d[v] != d[u] + e[i].f || inque[v]) continue;
rlow = dfs(v, min(rest, e[i].w));
if (rlow) {
e[i].w -= rlow;
e[i ^ 1].w += rlow;
rest -= rlow;
cost += e[i].f * rlow;
}
}
return flow - rest;
}
int main() {
n = read();
int las = n * 2;
T = las + 1;
p = read(), t1 = read(), a = read(), t2 = read(), b = read();
for (int i = 1; i <= n; ++ i) {
r[i] = read();
add(S, i + n, r[i], 0), add(i + n, S, 0, 0); // 源点连向结束点
add(i, T, r[i], 0), add(T, i, 0, 0); // 开始点连向汇点
add(S, i, r[i], p), add(i, S, 0, -p);
if (i + n + 1 <= las) { // 将脏餐巾留到明天晚上
add(i + n, i + n + 1, inf, 0);
add(i + n + 1, i + n, 0, 0);
}
if (i + t1 <= n) { // 送到快洗店
add(i + n, i + t1, inf, a);
add(i + t1, i + n, 0, -a);
}
if (i + t2 <= n) { // 送到慢洗店
add(i + n, i + t2, inf, b);
add(i + t2, i + n, 0, -b);
}
}
while (spfa()) {
for (int i = S; i <= T; ++ i) {
inque[i] = 0;
}
while (dfs(S, inf)) {
for (int i = S; i <= T; ++ i) {
inque[i] = 0;
}
}
}
printf("%lld\n", cost);
return 0;
}
10、「网络流 24 题」软件补丁
\(n\) 很小,似乎可以状压,最终要到达 \(0\) 状态(即没有错误)就是答案,根据补丁信息将一些状态之间连边,边权为补丁消耗的时间,跑最短路就行了(网络流?)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
inline ll read() {
ll x = 0;
int fg = 0;
char ch = getchar();
while (ch < '0' || ch > '9') {
fg |= (ch == '-');
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return fg ? ~x + 1 : x;
}
const int N = 22;
const int M = 105;
int n, m, s;
int t[M], b1[M], b2[M], f1[M], f2[M], dis[(1 << N) + 1];
char str[N];
int inque[(1 << N) + 1];
queue<int> q;
int main() {
n = read(), m = read();
s = (1ll << n) - 1;
for (int i = 1; i <= m; ++ i) {
t[i] = read();
cin >> str;
int siz = strlen(str);
for (int j = 0; j < siz; ++ j) {
if (str[j] == '+') {
b1[i] |= (1 << j);
}
if (str[j] == '-') {
b2[i] |= (1 << j);
}
}
scanf("%s", str);
siz = strlen(str);
for (int j = 0; j < siz; ++ j) {
if (str[j] == '-') {
f1[i] |= (1 << j);
}
if (str[j] == '+') {
f2[i] |= (1 << j);
}
}
}
for (int i = 0; i <= (1 << n); ++ i) {
dis[i] = 1e9;
}
dis[s] = 0;
q.push(s);
inque[s] = 1;
while (!q.empty()) {
int u = q.front();
for (int i = 1; i <= m; ++ i) {
if (((u & b1[i]) == b1[i]) && ((u & b2[i]) == 0)) {
int v = ((u | f1[i]) | f2[i]) ^ f1[i];
// 这里先 | f1[i] 再 ^ f1[i],为了防止原本没有此错误,一异或有了
if (dis[v] > dis[u] + t[i]) {
dis[v] = dis[u] + t[i];
if (!inque[v]) {
q.push(v);
inque[v] = 1;
}
}
}
}
q.pop();
inque[u] = 0;
}
if (dis[0] == 1e9) {
printf("0\n");
}
else {
printf("%d\n", dis[0]);
}
return 0;
}
11、「网络流 24 题」数字梯形
和那个最长递增子序列很想,处理三个问题,大体看一眼,第三题最简单,就是一个杨辉三角的递推 \(\text{DP}\),和网络流没关系
第二问,允许在数字节点处重合,边不能重复,那我们建边时将流量设为 \(1\) 即可,费用就是将杨辉三角的每个位置的权值下放到边上
第一问,数字节点和边都不能重复,那就在问题二的基础上,将节点拆开,节点之间连边,流量为 \(1\),费用为 \(0\),同时两个节点一个连接从前面推到这的,一个负责连接从这个点出发到达的其他点
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
inline ll read() {
ll x = 0;
int fg = 0;
char ch = getchar();
while (ch < '0' || ch > '9') {
fg |= (ch == '-');
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return fg ? ~x + 1 : x;
}
const int N = 105;
int m, n, cnt = 1, cot, S, T;
ll maxflow, cost;
int tap[N][N], h[N * N], id[N][N], cur[N * N];
ll dis[N * N], dp[N][N];
bool inque[N * N];
struct edge {
int v, nxt;
ll w, f;
} e[N * N << 3];
queue<int> q;
void add(int u, int v, ll w, ll f) {
e[++ cnt].v = v, e[cnt].nxt = h[u];
e[h[u] = cnt].w = w, e[cnt].f = f;
e[++ cnt].v = u, e[cnt].nxt = h[v];
e[h[v] = cnt].w = 0, e[cnt].f = -f;
}
int spfa() {
while (!q.empty()) q.pop();
for (int i = S; i <= T; ++ i) {
dis[i] = -1e9;
cur[i] = h[i];
}
q.push(S);
inque[S] = 1;
dis[S] = 0;
while (!q.empty()) {
int u = q.front();
for (int i = h[u]; i; i = e[i].nxt) {
int v = e[i].v;
if (dis[v] < dis[u] + e[i].f && e[i].w) {
dis[v] = dis[u] + e[i].f;
if (!inque[v]) {
q.push(v);
inque[v] = 1;
}
}
}
q.pop();
inque[u] = 0;
}
return dis[T] > -1e9;
}
ll dfs(int u, ll flow) {
if (u == T) {
maxflow += flow;
return flow;
}
inque[u] = 1;
ll rest = flow, rlow = 0;
for (int i = cur[u]; i && rest; i = e[i].nxt) {
int v = e[i].v;
cur[u] = i;
if (!inque[v] && dis[v] == dis[u] + e[i].f && e[i].w) {
if ((rlow = dfs(v, min(rest, e[i].w)))) {
rest -= rlow;
e[i].w -= rlow;
e[i ^ 1].w += rlow;
cost += rlow * e[i].f;
}
}
}
inque[u] = 0;
return flow - rest;
}
void work1() {
T = cot * 2 + 1;
for (int i = 1; i <= m; ++ i) {
add(S, id[1][i], 1, 0);
}
for (int i = 1; i < n; ++ i) {
for (int j = 1; j <= i + m - 1; ++ j) {
add(id[i][j], id[i][j] + cot, 1, 0);
add(id[i][j] + cot, id[i + 1][j], 1, tap[i][j]);
add(id[i][j] + cot, id[i + 1][j + 1], 1, tap[i][j]);
}
}
for (int i = 1; i <= n + m - 1; ++ i) {
add(id[n][i], id[n][i] + cot, 1, 0);
add(id[n][i] + cot, T, 1, tap[n][i]);
}
while (spfa()) {
while (dfs(S, 1e9));
}
printf("%lld\n", cost);
}
void work2() {
T = cot + 1;
for (int i = 1; i <= m; ++ i) {
add(S, id[1][i], 1, 0);
}
for (int i = 1; i < n; ++ i) {
for (int j = 1; j <= m + i - 1; ++ j) {
add(id[i][j], id[i + 1][j], 1, tap[i][j]);
add(id[i][j], id[i + 1][j + 1], 1, tap[i][j]);
}
}
for (int i = 1; i <= m + n - 1; ++ i) {
add(id[n][i], T, 1e9, tap[n][i]);
}
while (spfa()) {
while (dfs(S, 1e9));
}
printf("%lld\n", cost);
}
void work3() {
for (int i = 1; i <= n + m - 1; ++ i) {
dp[n][i] = tap[n][i];
}
for (int i = n - 1; i; -- i) {
for (int j = 1; j <= i + m - 1; ++ j) {
dp[i][j] = max(dp[i + 1][j], dp[i + 1][j + 1]) + tap[i][j];
}
}
ll ans = 0;
for (int i = 1; i <= m; ++ i) {
ans += dp[1][i];
}
printf("%lld\n", ans);
}
void clr() {
cnt = 1;
cost = 0;
memset(e, 0, sizeof e);
memset(dis, 0, sizeof dis);
memset(inque, 0, sizeof inque);
memset(h, 0, sizeof h);
}
int main() {
m = read(), n = read();
for (int i = 1; i <= n; ++ i) {
for (int j = 1; j <= m + i - 1; ++ j) {
tap[i][j] = read();
id[i][j] = ++ cot;
}
}
work1();
clr();
work2();
clr();
work3();
return 0;
}
12、「网络流 24 题」运输问题
很模板的题了,源点向仓库连边,流量为 \(a_i\),费用为 \(0\),商店向汇点连边,流量为 \(b_i\),费用为 \(0\),仓库和商店之间连边,费用为 \(c_{i, j}\),流量为 \(+ \infty\),跑最小费用最大流,然后还原边,跑最大费用最大流,还原边操作就是将反向边的流量返还给正向边
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
inline ll read() {
ll x = 0;
int fg = 0;
char ch = getchar();
while (ch < '0' || ch > '9') {
fg |= (ch == '-');
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return fg ? ~x + 1 : x;
}
const int N = 110;
int n, m, cnt = 1, S, T;
ll cost;
int cur[N << 1], h[N << 1];
ll a[N], b[N], c[N][N], dis[N << 1];
bool vis[N << 1];
struct edge {
int v, nxt;
ll w, f;
} e[N * N << 2];
void add(int u, int v, ll w, ll f) {
e[++ cnt].v = v, e[cnt].nxt = h[u], e[h[u] = cnt].w = w, e[cnt].f = f;
e[++ cnt].v = u, e[cnt].nxt = h[v], e[h[v] = cnt].w = 0, e[cnt].f = -f;
}
int spfa1() {
for (int i = S; i <= T; ++ i) {
cur[i] = h[i];
dis[i] = 1e9;
}
queue<int> q;
q.push(S);
dis[S] = 0;
vis[S] = 1;
while (!q.empty()) {
int u = q.front();
for (int i = h[u]; i; i = e[i].nxt) {
int v = e[i].v;
if (e[i].w && dis[v] > dis[u] + e[i].f) {
dis[v] = dis[u] + e[i].f;
if (!vis[v]) {
vis[v] = 1;
q.push(v);
}
}
}
q.pop();
vis[u] = 0;
}
return dis[T] < 1e9;
}
int spfa2() {
for (int i = S; i <= T; ++ i) {
cur[i] = h[i];
dis[i] = -1e9;
}
queue<int> q;
q.push(S);
dis[S] = 0;
vis[S] = 1;
while (!q.empty()) {
int u = q.front();
for (int i = h[u]; i; i = e[i].nxt) {
int v = e[i].v;
if (e[i].w && dis[v] < dis[u] + e[i].f) {
dis[v] = dis[u] + e[i].f;
if (!vis[v]) {
vis[v] = 1;
q.push(v);
}
}
}
q.pop();
vis[u] = 0;
}
return dis[T] > -1e9;
}
ll dfs(int u, ll flow) {
if (u == T) {
return flow;
}
vis[u] = 1;
ll rest = flow, rlow = 0;
for (int i = cur[u]; i && rest; i = e[i].nxt) {
cur[u] = i;
int v = e[i].v;
if (dis[v] == dis[u] + e[i].f && e[i].w && !vis[v]) {
if ((rlow = dfs(v, min(rest, e[i].w)))) {
e[i].w -= rlow;
e[i ^ 1].w += rlow;
rest -= rlow;
cost += rlow * e[i].f;
}
}
}
vis[u] = 0;
return flow - rest;
}
int main() {
m = read(), n = read();
T = n + m + 1;
for (int i = 1; i <= m; ++ i) {
a[i] = read();
add(S, i, a[i], 0); // 源点提供货物
}
for (int i = 1; i <= n; ++ i) {
b[i] = read();
add(i + m, T, b[i], 0); // 向汇点送货物
}
for (int i = 1; i <= m; ++ i) {
for (int j = 1; j <= n; ++ j) {
c[i][j] = read();
add(i, j + m, 1e18, c[i][j]);
}
}
while (spfa1()) {
while (dfs(S, 1e18));
}
printf("%lld\n", cost);
cost = 0;
for (int i = 2; i <= cnt; i += 2) {
e[i].w += e[i ^ 1].w;
e[i ^ 1].w = 0;
}
while (spfa2()) {
while (dfs(S, 1e18));
}
printf("%lld\n", cost);
return 0;
}
13、「网络流 24 题」分配问题
也是一道很模板的题甚至上一题的代码改改建边和输入就能过,源点向人连边,费用为 \(0\),流量为 \(1\),工作向汇点连边,费用为 \(0\),费用为 \(1\),人和工作之间连边,费用为 \(c_{i, j}\),流量为 \(1\),跑最小费用最大流和最大费用最大流即可
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
inline ll read() {
ll x = 0;
int fg = 0;
char ch = getchar();
while (ch < '0' || ch > '9') {
fg |= (ch == '-');
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return fg ? ~x + 1 : x;
}
const int N = 110;
int n, m, cnt = 1, S, T, cot;
ll cost;
int cur[N << 1], h[N << 1], id[N][N];
ll c[N][N], dis[N << 1];
bool vis[N << 1];
struct edge {
int v, nxt;
ll w, f;
} e[N * N << 2];
void add(int u, int v, ll w, ll f) {
e[++ cnt].v = v, e[cnt].nxt = h[u], e[h[u] = cnt].w = w, e[cnt].f = f;
e[++ cnt].v = u, e[cnt].nxt = h[v], e[h[v] = cnt].w = 0, e[cnt].f = -f;
}
int spfa1() {
for (int i = S; i <= T; ++ i) {
cur[i] = h[i];
dis[i] = 1e9;
}
queue<int> q;
q.push(S);
dis[S] = 0;
vis[S] = 1;
while (!q.empty()) {
int u = q.front();
for (int i = h[u]; i; i = e[i].nxt) {
int v = e[i].v;
if (e[i].w && dis[v] > dis[u] + e[i].f) {
dis[v] = dis[u] + e[i].f;
if (!vis[v]) {
vis[v] = 1;
q.push(v);
}
}
}
q.pop();
vis[u] = 0;
}
return dis[T] < 1e9;
}
int spfa2() {
for (int i = S; i <= T; ++ i) {
cur[i] = h[i];
dis[i] = -1e9;
}
queue<int> q;
q.push(S);
dis[S] = 0;
vis[S] = 1;
while (!q.empty()) {
int u = q.front();
for (int i = h[u]; i; i = e[i].nxt) {
int v = e[i].v;
if (e[i].w && dis[v] < dis[u] + e[i].f) {
dis[v] = dis[u] + e[i].f;
if (!vis[v]) {
vis[v] = 1;
q.push(v);
}
}
}
q.pop();
vis[u] = 0;
}
return dis[T] > -1e9;
}
ll dfs(int u, ll flow) {
if (u == T) {
return flow;
}
vis[u] = 1;
ll rest = flow, rlow = 0;
for (int &i = cur[u]; i && rest; i = e[i].nxt) {
int v = e[i].v;
if (dis[v] == dis[u] + e[i].f && e[i].w && !vis[v]) {
if ((rlow = dfs(v, min(rest, e[i].w)))) {
e[i].w -= rlow;
e[i ^ 1].w += rlow;
rest -= rlow;
cost += rlow * e[i].f;
}
}
}
vis[u] = 0;
return flow - rest;
}
int main() {
n = read();
T = n + n + n + 1;
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) {
c[i][j] = read();
add(i, j + n, 1, c[i][j]);
}
}
while (spfa1()) {
while (dfs(S, 1e18));
}
printf("%lld\n", cost);
cost = 0;
for (int i = 2; i <= cnt; i += 2) {
e[i].w += e[i ^ 1].w;
e[i ^ 1].w = 0;
}
while (spfa2()) {
while (dfs(S, 1e18));
}
printf("%lld\n", cost);
return 0;
}
14、「网络流 24 题」负载平衡
这个题要注意,每个仓库只能向相邻的两个仓库运送货物,第 \(n + 1\) 个仓库就是第 \(1\) 个仓库(环),同时最后要求每个仓库中的货物数量都相等,预先处理出平均值,如果一个仓库的货物量大于平均值,那我们就要将多的货物运出去,从仓库向汇点连一条边,流量为 \(a_i - aver\) (\(a_i\) 表示第 \(i\) 个仓库的货物数),如果货物量小于平均值,那就从别处运进货物,从源点向仓库连接一条边,流量为 \(aver - a_i\),相邻的仓库之间也要连边,流量为 \(+ \infty\),费用为 \(1\),跑最小费用最大流即可
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
inline ll read() {
ll x = 0;
int fg = 0;
char ch = getchar();
while (ch < '0' || ch > '9') {
fg |= (ch == '-');
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return fg ? ~x + 1 : x;
}
const int N = 110;
const ll inf = 1e18;
int n, S, T, cnt = 1;
ll cost, sum;
int h[N], cur[N];
ll dis[N], a[N];
bool vis[N];
struct edge {
int v, nxt;
ll w, f;
} e[N * N + N * 3];
void add(int u, int v, ll w, ll f) {
e[++ cnt].v = v, e[cnt].nxt = h[u], e[h[u] = cnt].w = w, e[cnt].f = f;
e[++ cnt].v = u, e[cnt].nxt = h[v], e[h[v] = cnt].w = 0, e[cnt].f = -f;
}
ll spfa() {
for (int i = S; i <= T; ++ i) {
dis[i] = 1e18;
cur[i] = h[i];
}
deque<int> q;
q.push_back(S);
vis[S] = 1, dis[S] = 0;
while (!q.empty()) {
int u = q.front();
q.pop_front();
vis[u] = 0;
for (int i = h[u]; i; i = e[i].nxt) {
int v = e[i].v;
if (dis[v] > dis[u] + e[i].f && e[i].w) {
dis[v] = dis[u] + e[i].f;
if (!vis[v]) {
vis[v] = 1;
if (!q.empty() && dis[v] <= dis[q.front()]) {
q.push_front(v);
}
else {
q.push_back(v);
}
}
}
}
}
return dis[T] < 1e18;
}
ll dfs(int u, ll flow) {
if (u == T) {
return flow;
}
vis[u] = 1;
ll rest = flow, rlow = 0;
for (int &i = cur[u]; i && rest; i = e[i].nxt) {
int v = e[i].v;
if (dis[v] == dis[u] + e[i].f && !vis[v] && e[i].w) {
if ((rlow = dfs(v, min(rest, e[i].w)))) {
e[i].w -= rlow;
e[i ^ 1].w += rlow;
rest -= rlow;
cost += rlow * e[i].f;
}
}
}
vis[u] = 0;
return flow - rest;
}
int main() {
n = read();
T = n + 1;
for (int i = 1; i <= n; ++ i) {
a[i] = read();
sum += a[i];
(i == 1) ? add(i, n, inf, 1) : add(i, i - 1, inf, 1);
(i == n) ? add(i, 1, inf, 1) : add(i, i + 1, inf, 1);
}
sum /= n;
for (int i = 1; i <= n; ++ i) {
if (a[i] > sum) {
add(i, T, a[i] - sum, 0);
}
if (a[i] < sum) {
add(S, i, sum - a[i], 0);
}
}
while (spfa()) {
while (dfs(S, 1e18));
}
printf("%lld\n", cost);
return 0;
}
15、「网络流 24 题」最长 k 可重区间集
题目大意,找一些线段,保证数轴上每个点被覆盖不超过 \(k\) 次,我们从线段入手,线段会有重叠和不重叠两种情况,如果没有重叠的线段,我们放心大胆的选就行,不用担心有的点会被覆盖超过 \(k\) 次,对于重叠的线段,我们就不能不过脑子直接选了
根据上面的分析建图,对于不重叠的边,我们连一条边,流量为 \(1\),费用为 \(0\),对于重叠的边,不能直接选,那我们就不连边,每个区间只能选一次,所以将区间拆成两个点,两个点之间连边,流量为 \(1\),费用为这个区间的长度,同时,我们要从源点流入 \(k\) 的流量,这样可以保证每个点不会被覆盖超过 \(k\) 次(不好理解可以这么想,假设所有的区间都相等,即所有区间都重叠,那我们最多只能选 \(k\) 个区间,所以流量为 \(k\)),但如何才能保证只有 \(k\) 的流量会流入这个网络流系统?再建一个源点 \(\_S\),将源点 \(S\) 与 \(\_S\) 连一条流量为 \(k\) 的边,这样就 \(OK\) 了,\(\_S\) 向每一个区间连一条流量为 \(1\),费用为 \(0\) 的边,同时每一个区间在向汇点连一条流量为 \(1\),费用为 \(0\) 的边,建完图后跑最大费用最大流
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
inline ll read() {
ll x = 0;
int fg = 0;
char ch = getchar();
while (ch < '0' || ch > '9') {
fg |= (ch == '-');
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return fg ? ~x + 1 : x;
}
const int N = 510;
const ll inf = 1e18;
int n, k, cnt = 1, S, T, _S;
ll cost;
int h[N << 1], cur[N << 1];
ll dis[N << 1];
bool vis[N << 1];
struct edge {
int v, nxt;
ll w, f;
} e[N * N << 2];
struct mat {
int l, r, len;
bool operator < (const mat &b) {
return (l == b.l) ? r < b.r : l < b.l;
}
} m[N];
void add(int u, int v, ll w, ll f) {
e[++ cnt].v = v, e[cnt].nxt = h[u], e[h[u] = cnt].w = w, e[cnt].f = f;
e[++ cnt].v = u, e[cnt].nxt = h[v], e[h[v] = cnt].w = 0, e[cnt].f = -f;
}
bool spfa() {
for (int i = S; i <= T; ++ i) {
dis[i] = -1e9;
cur[i] = h[i];
}
deque<int> q;
q.push_back(S);
vis[S] = 1;
dis[S] = 0;
while (!q.empty()) {
int u = q.front();
q.pop_front();
for (int i = h[u]; i; i = e[i].nxt) {
int v = e[i].v;
if (dis[v] < dis[u] + e[i].f && e[i].w) {
dis[v] = dis[u] + e[i].f;
if (!vis[v]) {
vis[v] = 1;
if (!q.empty() && dis[v] >= dis[q.front()]) {
q.push_front(v);
}
else {
q.push_back(v);
}
}
}
}
vis[u] = 0;
}
return dis[T] > -1e9;
}
ll dfs(int u, ll flow) {
if (u == T) {
return flow;
}
vis[u] = 1;
ll rlow = 0, rest = flow;
for (int &i = cur[u]; i && rest; i = e[i].nxt) {
int v = e[i].v;
if (dis[v] == dis[u] + e[i].f && e[i].w && !vis[v]) {
if ((rlow = dfs(v, min(rest, e[i].w)))) {
e[i].w -= rlow;
e[i ^ 1].w += rlow;
rest -= rlow;
cost += e[i].f * rlow;
}
}
}
vis[u] = 0;
return flow - rest;
}
int main() {
n = read(), k = read();
_S = 1, T = n * 2 + 2;
add(S, _S, k, 0);
for (int i = 1; i <= n; ++ i) {
m[i].l = read(), m[i].r = read();
if (m[i].l > m[i].r) swap(m[i].l, m[i].r);
}
sort(m + 1, m + n + 1);
for (int i = 1; i <= n; ++ i) {
add(i + 1, i + n + 1, 1, (m[i].len = m[i].r - m[i].l));
add(i + n + 1, T, 1, 0);
add(_S, i + 1, 1, 0);
}
for (int i = 1; i < n; ++ i) {
for (int j = i + 1; j <= n; ++ j) {
if (m[i].l + m[i].len <= m[j].l) {
add(i + n + 1, j + 1, 1, 0);
}
}
}
while (spfa()) {
while (dfs(S, 1e18));
}
printf("%lld\n", cost);
return 0;
}
16、「网络流 24 题」星际转移
这道题有点分层图的意思了,先分析题目,对于人来说,可以选择暂时留在一个太空站上,也可以选择乘坐太空船去别的地方,最后的目的地是月球
按照天数分层,对于第 \(i\) 天的第 \(j\) 个太空站可以直接连向第 \(i + 1\) 天的第 \(j\) 个太空站,也可以根据飞船的路程连向第 \(i + 1\) 天的飞船能到达的下一个太空站,将每一天的月球的位置向汇点连一条边,流量为 \(+ \infty\),源点点向第 \(0\) 天的地球连边,流量为地球的人数,跑最大流,每天都跑一次最大流,知道最大流超过 \(k\),代表人已经全到月球上了
判断是否有解就是判断图是否连通,并查集即可
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define get(x, y) (x + (y * (n + 2)))
inline ll read() {
ll x = 0;
int fg = 0;
char ch = getchar();
while (ch < '0' || ch > '9') {
fg |= (ch == '-');
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return fg ? ~x + 1 : x;
}
const int N = 15;
const int M = 25;
const int K = 55;
const ll inf = 1e18;
int n, m, k, S, T, cnt = 1;
ll maxflow;
int num[M], r[M], s[M][N], fa[N];
int dep[N * 10000], cur[N * 10000], h[N * 10000];
bool vis[N * 10000];
struct edge {
int v, nxt;
ll w;
} e[N * N * 10005];
int find(int x) {
return fa[x] == x ? fa[x] : fa[x] = find(fa[x]);
}
inline void Union(int x, int y) {
int fx = find(x), fy = find(y);
if (fx != fy) fa[fx] = fy;
}
inline void add(int u, int v, ll w) {
e[++ cnt].v = v, e[cnt].nxt = h[u], e[h[u] = cnt].w = w;
e[++ cnt].v = u, e[cnt].nxt = h[v], e[h[v] = cnt].w = 0;
}
bool bfs() {
for (int i = 0; i <= T; ++ i) {
dep[i] = 1e9;
vis[i] = 0;
}
queue<int> q;
q.push(S);
dep[S] = 0;
cur[S] = h[S];
while (!q.empty()) {
int u = q.front();
q.pop();
vis[u] = 0;
for (int i = h[u]; i; i = e[i].nxt) {
int v = e[i].v;
if (e[i].w && dep[v] > dep[u] + 1) {
dep[v] = dep[u] + 1;
cur[v] = h[v];
if (!vis[v]) {
vis[v] = 1;
q.push(v);
}
if (v == T) return 1;
}
}
}
return 0;
}
ll dfs(int u, ll flow) {
if (u == T || flow == 0) {
maxflow += flow;
return flow;
}
ll rlow = 0, rest = flow;
for (int &i = cur[u]; i && rest; i = e[i].nxt) {
int v = e[i].v;
if (e[i].w && dep[v] == dep[u] + 1) {
if ((rlow = dfs(v, min(rest, e[i].w)))) {
e[i].w -= rlow;
e[i ^ 1].w += rlow;
rest -= rlow;
}
else dep[v] = 1e9;
}
}
return flow - rest;
}
int main() {
n = read(), m = read(), k = read();
T = N * 10000 - 1;
S = N * 10000 - 2;
for (int i = 0; i <= n + 1; ++ i) fa[i] = i;
for (int i = 1; i <= m; ++ i) {
num[i] = read(), r[i] = read();
for (int j = 0; j < r[i]; ++ j) {
s[i][j] = read();
if (s[i][j] == -1) s[i][j] = n + 1;
if (j) Union(s[i][j - 1], s[i][j]);
}
}
if (find(0) != find(n + 1)) {
puts("0");
return 0;
}
add(S, get(0, 0), k);
add(get(n + 1, 0), T, inf);
int day = 1;
while (day) {
add(get(n + 1, day), T, inf);
for (int i = 0; i <= n + 1; ++ i) {
add(get(i, (day - 1)), get(i, day), inf);
}
for (int i = 1; i <= m; ++ i) {
int people = num[i], xh = r[i];
int a = s[i][(day - 1) % xh], b = s[i][day % xh];
add(get(a, (day - 1)), get(b, day), people);
}
while (bfs()) {
dfs(S, 1e18);
}
if (maxflow >= k) break;
++ day;
}
printf("%d\n", day);
return 0;
}
17、「网络流 24 题」孤岛营救
分层图,钥匙的种类很少,可以状压,我们就按照钥匙的状态分层,钥匙的状态:这一层有什么种类的钥匙(对于每种钥匙来说就是这一层是否有这种钥匙),跑分层图最短路
分层图最短路可以看一下这篇博客分层图最短路
代码中有注释
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define get(x, y) (y * sum + x)
inline ll read() {
ll x = 0;
int fg = 0;
char ch = getchar();
while (ch < '0' || ch > '9') {
fg |= (ch == '-');
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return fg ? ~x + 1 : x;
}
const int N = 20;
int n, m, p, k, cot, s, cnt, sum, tot, cs;
int id[N][N], fg[110][110], key[15][15], num[20], h[1000010];
ll dis[10000010];
bool vis[10000010], havekey[20];
struct edge {
int v, nxt;
ll w;
} e[10000010];
void input() {
n = read(), m = read(), p = read(), k = read();
for (int i = 1; i <= n; ++ i) {
for (int j = 1; j <= m; ++ j) {
id[i][j] = ++ cot; // 对点进行标号
}
}
for (int i = 1; i <= k; ++ i) {
int x = read(), y = read(), a, b;
a = id[x][y];
x = read(), y = read();
b = id[x][y];
x = read();
if (x == 0) x = -1;
fg[a][b] = fg[b][a] = x; // a到b是否通行
}
s = read();
for (int i = 1; i <= s; ++ i) {
int x = read(), y = read(), q = read();
++ num[q];
key[q][num[q]] = id[x][y]; // 记录位置
}
}
void add(int u, int v, ll w) {
e[++ cnt].v = v;
e[cnt].nxt = h[u];
e[h[u] = cnt].w = w;
}
void build() {
sum = n * m, cs = (1 << p), tot = sum * cs;
// sum 每一层的节点数
// cs 层数
// tot 总的节点数
for (int c = 0; c < cs; ++ c) {
for (int i = 1; i <= p; ++ i) {
if (c & (1 << (i - 1))) { // 是否有第i种钥匙
havekey[i] = 1;
}
else {
havekey[i] = 0;
}
}
for (int i = 1; i <= n; ++ i) {
for (int j = 1; j <= m; ++ j) {
int now = id[i][j], you = id[i][j + 1], xia = id[i + 1][j];
// 向右边的点和下边的点连边
if (you && ~fg[now][you]) { // 该点存在且没有墙
if (!fg[now][you] || havekey[fg[now][you]]) {
// 没有门或者有这个门的钥匙
add(get(now, c), get(you, c), 1);
add(get(you, c), get(now, c), 1); // 建立双向边
}
}
if (xia && ~fg[now][xia]) {
if (!fg[now][xia] || havekey[fg[now][xia]]) {
add(get(now, c), get(xia, c), 1);
add(get(xia, c), get(now, c), 1);
}
}
}
}
for (int i = 1; i <= p; ++ i) {
if (!havekey[i]) { // 没有第i种钥匙
int t = c + (1 << (i - 1)); // 有第i种钥匙的层
for (int j = 1; j <= num[i]; ++ j) {
int x = key[i][j]; // 第i种钥匙的位置
add(get(x, c), get(x, t), 0);
// 到达这个点,拿到钥匙,就进入有该钥匙的层了
}
}
}
}
}
void spfa() {
for (int i = 1; i <= tot; ++ i) {
dis[i] = 1e9;
}
queue<int> q;
q.push(id[1][1]);
dis[id[1][1]] = 0;
vis[id[1][1]] = 1;
while (!q.empty()) {
int u = q.front();
q.pop();
vis[u] = 0;
for (int i = h[u]; i; i = e[i].nxt) {
int v = e[i].v;
if (dis[v] > dis[u] + e[i].w) {
dis[v] = dis[u] + e[i].w;
if (!vis[v]) {
q.push(v);
vis[v] = 1;
}
}
}
}
}
int main() {
input();
build();
spfa();
int T = id[n][m];
ll ans = 1e9;
for (int i = 0; i < cs; ++ i) {
ans = min(ans, dis[get(T, i)]);
}
if (ans < 1e9) {
printf("%lld\n", ans);
}
else {
puts("-1");
}
return 0;
}
18、「网络流 24 题」航空路线
把题目改成人话就是从起点找两条不相交的能到终点的最长的路径,然后输出,除了起点和终点,其他点只能经过一次,只能经过一次,拆点连边流量为 \(1\) 呀,因为还要记录经过了几个点,所以拆的点之间费用为 \(1\),起点和终点也只能经过 \(2\) 次(毕竟只招 \(2\) 条路经),所以拆点,点之间连流量为 \(2\),费用为 \(1\) 的边?这里我们要想一个问题,如果我们这么建边,那到时候,起点和终点就多记了一次,即最后的最大流中多了 \(2\),减去就行了,这里还有一种解决方法,起点拆成的点之间连两条边,流量都为 \(1\),但一个有费用,一个没有费用,这样,就既保证了总流量为 \(2\),也保证了记录点数不会多记,终点也是一样的
然后根据飞机的直航路线连边,由于城市名都是字符串,所以用 map
来标号
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define bug puts("IOI AK ME!")
const int N = 110;
int n, m, cnt = 1, cot, S, T;
ll cost, maxflow;
int h[N << 1], cur[N << 1];
ll dis[N << 1];
bool vis[N << 1];
unordered_map<string, int> mp;
unordered_map<int, string> mp2;
vector<int> d1, d2;
struct edge {
int v, nxt;
ll w, f;
} e[N * N << 1];
inline void add(int u, int v, ll w, ll f) {
e[++ cnt].v = v, e[cnt].nxt = h[u], e[h[u] = cnt].w = w, e[cnt].f = f;
e[++ cnt].v = u, e[cnt].nxt = h[v], e[h[v] = cnt].w = 0, e[cnt].f = -f;
}
int spfa() {
for (int i = S; i <= T; ++ i) {
cur[i] = h[i];
dis[i] = -1e9;
}
deque<int> q;
q.push_back(S);
dis[S] = 0;
vis[S] = 1;
while (!q.empty()) {
int u = q.front();
q.pop_front();
for (int i = h[u]; i; i = e[i].nxt) {
int v = e[i].v;
if (dis[v] < dis[u] + e[i].f && e[i].w) {
dis[v] = dis[u] + e[i].f;
if (!vis[v]) {
vis[v] = 1;
if (!q.empty() && dis[v] >= dis[q.front()]) {
q.push_front(v);
}
else {
q.push_back(v);
}
}
}
}
vis[u] = 0;
}
return dis[T] > -1e9;
}
ll dfs(int u, ll flow) {
if (u == T) {
maxflow += flow;
return flow;
}
vis[u] = 1;
ll rest = flow, rlow = 0;
for (int &i = cur[u]; i && rest; i = e[i].nxt) {
int v = e[i].v;
if (dis[v] == dis[u] + e[i].f && e[i].w && !vis[v]) {
if ((rlow = dfs(v, min(rest, e[i].w)))) {
e[i].w -= rlow;
e[i ^ 1].w += rlow;
rest -= rlow;
cost += rlow * e[i].f;
}
}
}
vis[u] = 0;
return flow - rest;
}
void dfs1(int u) {
cout << mp2[u - n] << endl;
vis[u] = 1;
for (int i = h[u]; i; i = e[i].nxt) {
if (!e[i].w) {
int v = e[i].v;
if (v <= n) {
dfs1(v + n);
}
break;
}
}
}
void dfs2(int u) {
for (int i = h[u]; i; i = e[i].nxt) {
if (!e[i].w) {
int v = e[i].v;
if (vis[v + n]) continue;
if (v <= n) {
dfs2(v + n);
}
}
}
cout << mp2[u - n] << endl;
}
int main() {
int fg = 0;
scanf("%d%d", &n, &m);
T = n * 2 + 1;
add(S, 1, 2, 0);
add(2 * n, T, 2, 0);
for (int i = 1; i <= n; ++ i) {
string s;
cin >> s;
mp[s] = ++ cot;
mp2[cot] = s;
if (i != 1 && i != n) add(i, i + n, 1, 1);
else {
add(i, i + n, 1, 1);
add(i, i + n, 1, 0);
}
}
for (int i = 1, x, y; i <= m; ++ i) {
string a, b;
cin >> a >> b;
x = mp[a], y = mp[b];
if (x > y) swap(x, y);
add(x + n, y, 1, 0);
if (x == 1 && y == n) fg = 1;
}
while (spfa()) {
while(dfs(S, 1e18));
}
if (maxflow == 2) {
printf("%lld\n", cost);
}
else {
if (maxflow == 1 && fg) {
puts("2");
cout << mp2[1] << '\n' << mp2[n] << '\n' << mp2[1] << endl;
}
else {
puts("No Solution!");
}
return 0;
}
dfs1(1 + n);
dfs2(1 + n);
return 0;
}
19、「网络流 24 题」汽车加油行驶问题
又是一道分层图
直接说建图,我们按照已经使用的汽油量来分层,所以只会分 \(k\) 层,第 \(0\) 层则代表油箱是满的,第 \(k\) 层代表油箱已经空了
对于每一层之间的关系,如果经过了一个加油站,不管有还剩多少,都向第 \(0\) 层的这个点连边(因为加满了,而且是强制加油),如果这个位置不是加油站同时还有油,那就向下一层(如果当前是第 \(i\) 层,那下一层就是第 \(i + 1\) 层)的点连边,四个方向四个点,向上或向左走就有费用 \(b\),否则费用为 \(0\),那流量为多少呢?流量为 \(1\),因为只有一辆车,而且根据贪心思想,我们不会回到原来的点,所以设为 \(1\),如果油空了且没有加油站,那就建一个,连一条边到第 \(0\) 层的该点,费用为 \(a + c\),流量为 \(1\)
这里有个坑,如果到达终点时,油刚好用完,那就不用再建加油站了,题目中也说了,起点和终点不设加油站
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define get(x, y) (x + (y * n * n))
inline ll read() {
ll x = 0;
int fg = 0;
char ch = getchar();
while (ch < '0' || ch > '9') {
fg |= (ch == '-');
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return fg ? ~x + 1 : x;
}
const int N = 110;
int n, k, a, b, c, S, T, cot, cnt = 1;
ll cost;
int g[N][N], id[N][N], h[10000010], cur[10000010];
ll dis[10000010];
bool vis[10000010];
struct edge {
int v, nxt;
ll w, f;
} e[20000010];
inline void add(int u, int v, ll w, ll f) {
e[++ cnt].v = v, e[cnt].nxt = h[u], e[(h[u] = cnt)].w = w, e[cnt].f = f;
e[++ cnt].v = u, e[cnt].nxt = h[v], e[(h[v] = cnt)].w = 0, e[cnt].f = -f;
}
ll spfa() {
for (int i = 0; i <= T; ++ i) {
dis[i] = 1e9;
cur[i] = h[i];
}
deque<int> q;
q.push_back(S);
vis[S] = 1, dis[S] = 0;
while (!q.empty()) {
int u = q.front();
q.pop_front();
vis[u] = 0;
for (int i = h[u]; i; i = e[i].nxt) {
int v = e[i].v;
if (dis[v] > dis[u] + e[i].f && e[i].w) {
dis[v] = dis[u] + e[i].f;
if (!vis[v]) {
vis[v] = 1;
if (!q.empty() && dis[v] <= dis[q.front()]) {
q.push_front(v);
}
else {
q.push_back(v);
}
}
}
}
}
return dis[T] < 1e9;
}
ll dfs(int u, ll flow) {
if (u == T) {
return flow;
}
vis[u] = 1;
ll rest = flow, rlow = 0;
for (int &i = cur[u]; i && rest; i = e[i].nxt) {
int v = e[i].v;
if (dis[v] == dis[u] + e[i].f && e[i].w && !vis[v]) {
if ((rlow = dfs(v, min(rest, e[i].w)))) {
e[i].w -= rlow;
e[i ^ 1].w += rlow;
rest -= rlow;
cost += rlow * e[i].f;
}
}
}
vis[u] = 0;
return flow - rest;
}
// 按耗费的油量分层
int main() {
n = read(), k = read(), a = read(), b = read(), c = read();
T = n * n * (k + 1) + 1;
for (int i = 1; i <= n; ++ i) {
for (int j = 1; j <= n; ++ j) {
id[i][j] = ++ cot; // 编号
}
}
add(S, get(id[1][1], 0), 1, 0); // 源点向起始点连边
for (int i = 1; i <= k; ++ i) { // 向汇点连边
add(get(id[n][n], i), T, 1, 0);
}
for (int i = 1; i <= n; ++ i) { // 枚举每一个点
for (int j = 1; j <= n; ++ j) {
g[i][j] = read(); // 输入油站情况
if (g[i][j]) { // 有加油站,先加油
for (int l = 1; l <= k; ++ l) { // 不管油还剩多少,先加上
add(get(id[i][j], l), get(id[i][j], 0), 1, a);
}
// 下一步的运动
if (i < n) add(get(id[i][j], 0), get(id[i + 1][j], 1), 1, 0);
if (j < n) add(get(id[i][j], 0), get(id[i][j + 1], 1), 1, 0);
if (i > 1) add(get(id[i][j], 0), get(id[i - 1][j], 1), 1, b);
if (j > 1) add(get(id[i][j], 0), get(id[i][j - 1], 1), 1, b);
}
else { // 没有加油站
for (int l = 0; l < k; ++ l) { // 耗油量+1
if (i < n) add(get(id[i][j], l), get(id[i + 1][j], (l + 1)), 1, 0);
if (j < n) add(get(id[i][j], l), get(id[i][j + 1], (l + 1)), 1, 0);
if (i > 1) add(get(id[i][j], l), get(id[i - 1][j], (l + 1)), 1, b);
if (j > 1) add(get(id[i][j], l), get(id[i][j - 1], (l + 1)), 1, b);
}
add(get(id[i][j], k), get(id[i][j], 0), 1, a + c);// 没有加油站,自己建一个
}
}
}
while (spfa()) {
while (dfs(S, 1e18));
}
printf("%lld\n", cost);
return 0;
}
20、「网络流 24 题」深海机器人
这道题,出发点有多个,结束点也有多个,每条路径上都有一个费用,但这个费用用完就没了(一次性),我们可以利用上面的建边方式,每条边分成两种建,一种流量为 \(1\),费用为 \(1\),一种流量为 \(+ \infty\),费用为 \(0\),这样可以保证每条边的费用只会用一次,这样可以保证每条边的费用会被第一个经过的机器人带走吗?根据 \(\text{SPFA}\) 最长路的判断,很明显,走有费用的边更优,所以可以保证一定先走有费用的边,剩下的建图就很简单了,自己能想出来
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define bug cout << "IOI AK ME" << endl;
inline ll read() {
ll x = 0;
int fg = 0;
char ch = getchar();
while (ch < '0' || ch > '9') {
fg |= (ch == '-');
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return fg ? ~x + 1 : x;
}
const int N = 20;
const ll inf = 1e11;
int a, b, n, m, cnt = 1, cot, T, S;
ll cost;
int h[N * N], cur[N * N], id[N][N];
ll dis[N * N];
bool vis[N * N];
struct edge {
int v, nxt;
ll w, f;
} e[20000010];
inline void add(int u, int v, ll w, ll f) {
e[++ cnt].v = v, e[cnt].nxt = h[u], e[h[u] = cnt].w = w, e[cnt].f = f;
e[++ cnt].v = u, e[cnt].nxt = h[v], e[h[v] = cnt].w = 0, e[cnt].f = -f;
}
int spfa() {
for (int i = S; i <= T; ++ i) {
dis[i] = -inf;
cur[i] = h[i];
}
deque<int> q;
q.push_back(S);
dis[S] = 0, vis[S] = 1;
while (!q.empty()) {
int u = q.front();
q.pop_front();
for (int i = h[u]; i; i = e[i].nxt) {
int v = e[i].v;
if (dis[v] < dis[u] + e[i].f && e[i].w) {
dis[v] = dis[u] + e[i].f;
if (!vis[v]) {
vis[v] = 1;
if (!q.empty() && dis[v] >= dis[q.front()]) {
q.push_front(v);
}
else {
q.push_back(v);
}
}
}
}
vis[u] = 0;
}
return dis[T] > -inf;
}
ll dfs(int u, ll flow) {
if (u == T) {
return flow;
}
vis[u] = 1;
ll rest = flow, rlow = 0;
for (int &i = cur[u]; i && rest; i = e[i].nxt) {
int v = e[i].v;
if (!vis[v] && dis[v] == dis[u] + e[i].f && e[i].w) {
if ((rlow = dfs(v, min(rest, e[i].w)))) {
rest -= rlow;
e[i].w -= rlow;
e[i ^ 1].w += rlow;
cost += rlow * e[i].f;
// e[i].f = 0;
}
}
}
vis[u] = 0;
return flow - rest;
}
int main() {
a = read(), b = read();
n = read(), m = read();
T = (n + 1) * (m + 1) + 1;
for (int i = 0; i <= n; ++ i) {
for (int j = 0; j <= m; ++ j) {
id[i][j] = ++ cot;
}
}
for (int i = 0; i <= n; ++ i) {
for (int j = 0; j < m; ++ j) {
ll x = read();
add(id[i][j], id[i][j + 1], 1, x);
add(id[i][j], id[i][j + 1], inf, 0);
}
}
for (int i = 0; i <= m; ++ i) {
for (int j = 0; j < n; ++ j) {
ll x = read();
add(id[j][i], id[j + 1][i], 1, x);
add(id[j][i], id[j + 1][i], inf, 0);
}
}
for (int i = 1; i <= a; ++ i) {
ll z = read();
int x = read(), y = read();
add(S, id[x][y], z, 0);
}
for (int i = 1; i <= b; ++ i) {
ll z = read();
int x = read(), y = read();
add(id[x][y], T, z, 0);
}
while (spfa()) {
while (dfs(S, inf));
}
printf("%lld\n", cost);
return 0;
}
21、「网络流 24 题」火星探险
和上一个题大部分都是一样的,只是有些小变化,第一,有些点不能连边,这个判断一下就行了,第二,上一个题是边权,这个题是点权,这个也很简单,插点操作就可以把点权转化为边权,至于拆开的点之间的建边,还是建两种,和上题一样,而对于每个格子之间的建边,流量为 \(+ \infty\),费用为 \(0\),源点向出发点连边,结束点向汇点连边,最后要输出行走方案,所以对边,我们还要再设一个计数的值,如果这个值等于它的反向边的流量,那这个边就不能走了
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define bug cout << "IOI AK ME" << endl;
inline ll read() {
ll x = 0;
int fg = 0;
char ch = getchar();
while (ch < '0' || ch > '9') {
fg |= (ch == '-');
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return fg ? ~x + 1 : x;
}
const int N = 50;
const ll inf = 1e11;
int a, n, m, cnt = 1, cot, T, S;
ll cost, maxflow;
int h[N * N * 2], cur[N * N * 2], id[N][N], g[N][N];
ll dis[N * N * 2];
bool vis[N * N * 2];
struct edge {
int v, nxt;
ll w, f, jl;
} e[20000010];
inline void add(int u, int v, ll w, ll f) {
e[++ cnt].v = v, e[cnt].nxt = h[u], e[h[u] = cnt].w = w, e[cnt].f = f, e[cnt].jl = 0;
e[++ cnt].v = u, e[cnt].nxt = h[v], e[h[v] = cnt].w = 0, e[cnt].f = -f, e[cnt].jl = 0;
}
int spfa() {
for (int i = S; i <= T; ++ i) {
dis[i] = -inf;
cur[i] = h[i];
}
deque<int> q;
q.push_back(S);
dis[S] = 0, vis[S] = 1;
while (!q.empty()) {
int u = q.front();
q.pop_front();
for (int i = h[u]; i; i = e[i].nxt) {
int v = e[i].v;
if (dis[v] < dis[u] + e[i].f && e[i].w) {
dis[v] = dis[u] + e[i].f;
if (!vis[v]) {
vis[v] = 1;
if (!q.empty() && dis[v] >= dis[q.front()]) {
q.push_front(v);
}
else {
q.push_back(v);
}
}
}
}
vis[u] = 0;
}
return dis[T] > -inf;
}
ll dfs(int u, ll flow) {
if (u == T) {
maxflow += flow;
return flow;
}
vis[u] = 1;
ll rest = flow, rlow = 0;
for (int &i = cur[u]; i && rest; i = e[i].nxt) {
int v = e[i].v;
if (!vis[v] && dis[v] == dis[u] + e[i].f && e[i].w) {
if ((rlow = dfs(v, min(rest, e[i].w)))) {
rest -= rlow;
e[i].w -= rlow;
e[i ^ 1].w += rlow;
cost += rlow * e[i].f;
}
}
}
vis[u] = 0;
return flow - rest;
}
void dfs1(int u, int c) {
for (int i = h[u]; i; i = e[i].nxt) {
if (e[i ^ 1].w != e[i].jl) {
++ e[i].jl;
int v = e[i].v;
if (v != T) {
if (v + cot == u + 1) {
printf("%d 1\n", c);
}
else {
printf("%d 0\n", c);
}
}
dfs1(v + cot, c);
break;
}
}
}
int main() {
a = read(), m = read(), n = read();
T = n * m * 2 + 1;
for (int i = 1; i <= n; ++ i) {
for (int j = 1; j <= m; ++ j) {
id[i][j] = ++ cot;
g[i][j] = read();
}
}
for (int i = 1; i <= n; ++ i) {
for (int j = 1; j <= m; ++ j) {
if (g[i][j] == 1) continue;
if (g[i][j] == 2) {
add(id[i][j], id[i][j] + cot, 1, 1);
}
add(id[i][j], id[i][j] + cot, inf, 0);
if (i < n && g[i + 1][j] != 1) {
add(id[i][j] + cot, id[i + 1][j], inf, 0);
}
if (j < m && g[i][j + 1] != 1) {
add(id[i][j] + cot, id[i][j + 1], inf, 0);
}
}
}
add(S, id[1][1], a, 0);
add(id[n][m] + cot, T, a, 0);
while (spfa()) {
while (dfs(S, inf));
}
for (int i = 1; i <= maxflow; ++ i) {
dfs1(id[1][1] + cot, i);
}
return 0;
}
22、「网络流 24 题」骑士共存
这道题和上面的方格取数算是同一类型的,做法也差不多,观察题目给的图发现还是可以分成奇数和偶数两类点,源点向奇数类点连边,偶数类点向汇点连边,奇数类点和偶数类点之间根据条件连边,跑最大流最小割即可
连边和统计答案的时候,别忘了有些格子不能放骑士!
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
inline ll read() {
ll x = 0;
int fg = 0;
char ch = getchar();
while (ch < '0' || ch > '9') {
fg |= (ch == '-');
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return fg ? ~x + 1 : x;
}
const int N = 210;
const ll inf = 1e11;
const int fx[8] = {1, 2, 2, 1, -1, -2, -2, -1};
const int fy[8] = {2, 1, -1, -2, -2, -1, 1, 2};
int n, m, cot, cnt = 1, T, S;
ll maxflow;
int id[N][N], h[N * N], cur[N * N], dep[N * N];
int g[N][N], vis[N * N];
struct edge {
int v, nxt;
ll w;
} e[N * N << 4];
inline void add(int u, int v, ll w) {
e[++ cnt].v = v, e[cnt].nxt = h[u], e[h[u] = cnt].w = w;
e[++ cnt].v = u, e[cnt].nxt = h[v], e[h[v] = cnt].w = 0;
}
int bfs() {
for (int i = S; i <= T; ++ i) {
dep[i] = 1e9;
cur[i] = h[i];
vis[i] = 0;
}
queue<int> q;
q.push(S);
dep[S] = 0, vis[S] = 1;
while (!q.empty()) {
int u = q.front();
q.pop(), vis[u] = 0;
for (int i = h[u]; i; i = e[i].nxt) {
int v = e[i].v;
if (dep[v] > dep[u] + 1 && e[i].w) {
dep[v] = dep[u] + 1;
if (v == T) return 1;
if (!vis[v]) {
vis[v] = 1;
q.push(v);
}
}
}
}
return 0;
}
ll dfs(int u, ll flow) {
if (u == T) {
maxflow += flow;
return flow;
}
ll rlow = 0, rest = flow;
for (int &i = cur[u]; i && rest; i = e[i].nxt) {
int v = e[i].v;
if (dep[v] == dep[u] + 1 && e[i].w) {
if ((rlow = dfs(v, min(rest, e[i].w)))) {
e[i].w -= rlow;
e[i ^ 1].w += rlow;
rest -= rlow;
}
}
}
return flow - rest;
}
int main() {
n = read(), m = read();
for (int i = 1; i <= n; ++ i) {
for (int j = 1; j <= n; ++ j) {
id[i][j] = ++ cot;
}
}
for (int i = 1; i <= m; ++ i) {
int x = read(), y = read();
g[x][y] = 1;
}
T = n * n + 1;
for (int i = 1; i <= n; ++ i) {
for (int j = 1; j <= n; ++ j) {
if (g[i][j]) continue;
if ((i + j) & 1) {
add(S, id[i][j], 1);
for (int k = 0; k < 8; ++ k) {
int x = i + fx[k], y = j + fy[k];
if (!g[x][y] && x >= 1 && x <= n && y >= 1 && y <= n) {
add(id[i][j], id[x][y], inf);
}
}
}
else {
add(id[i][j], T, 1);
}
}
}
while (bfs()) {
dfs(S, inf);
}
printf("%lld\n", n * n - m - maxflow);
return 0;
}
23、「网络流 24 题」最长 k 可重线段集
这道题与前面的最长k可重区间集很想,但这题的坑更多,我们可以将线段的左右端点映射到数轴上,但是,有一种很特殊的情况,如果我们只是单纯的把左右端点映射到数轴上,那么,对于与 \(x\) 轴垂直的线段,如果就这么映射上去,那这条线段都不存在!怎么办呢,这里有一个做法——扩域,就是将所有线段左右端点都乘上 \(2\) 再映射,如果左右端点相同,那右端点向右移一位(毕竟是开区间),即 \((p_1, p_2)\) 变成 \((p_1 \times 2, p_2 \times 2 + 1)\),但是这样也有一个问题,不扩域前映射到数轴上的不重叠的线段,可能扩域后就重叠了,如 \((p_1, p_2), (p_3, p_4), p_3 = p_2\),一扩域后,就变成了 \((2p_1, 2p_2 + 1), (2p_3, 2p_4)\),这两条线段就有重叠部分了,这怎么办呢?我们把 \(2p_3\) 也加上 \(1\),就行了
剩下的就跟那道题差不多了
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define bug puts("IOI AK ME!");
inline ll read() {
ll x = 0;
int fg = 0;
char ch = getchar();
while (ch < '0' || ch > '9') {
fg |= (ch == '-');
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return fg ? ~x + 1 : x;
}
const int N = 510;
const ll inf = 1e18;
int n, k, cnt = 1, T, S, _S;
ll cost;
int h[N << 1], cur[N << 1];
ll dis[N << 1];
bool vis[N << 1];
struct stg {
int xl, yl, xr, yr;
ll len;
int operator < (const stg &b) {
return xl == b.xl ? xr < b.xr : xl < b.xl;
}
} s[510];
struct edge {
int v, nxt;
ll w, f;
} e[N * N + 2 * N + 10];
void add(int u, int v, ll w, ll f) {
e[++ cnt].v = v, e[cnt].nxt = h[u], e[h[u] = cnt].w = w, e[cnt].f = f;
e[++ cnt].v = u, e[cnt].nxt = h[v], e[h[v] = cnt].w = 0, e[cnt].f = -f;
}
int spfa() {
for (int i = S; i <= T; ++ i) {
dis[i] = -inf;
cur[i] = h[i];
}
deque<int> q;
q.push_back(S);
dis[S] = 0, vis[S] = 1;
while (!q.empty()) {
int u = q.front();
q.pop_front();
for (int i = h[u]; i; i = e[i].nxt) {
int v = e[i].v;
if (dis[v] < dis[u] + e[i].f && e[i].w) {
dis[v] = dis[u] + e[i].f;
if (!vis[v]) {
vis[v] = 1;
if (!q.empty() && dis[v] >= dis[q.front()]) {
q.push_front(v);
}
else {
q.push_back(v);
}
}
}
}
vis[u] = 0;
}
return dis[T] > -inf;
}
ll dfs(int u, ll flow) {
if (u == T) {
return flow;
}
vis[u] = 1;
ll rlow = 0, rest = flow;
for (int &i = cur[u]; i && rest; i = e[i].nxt) {
int v = e[i].v;
if (dis[v] == dis[u] + e[i].f && e[i].w && !vis[v]) {
if ((rlow = dfs(v, min(rest, e[i].w)))) {
e[i].w -= rlow;
e[i ^ 1].w += rlow;
rest -= rlow;
cost += e[i].f * rlow;
}
}
}
vis[u] = 0;
return flow - rest;
}
int main() {
n = read(), k = read();
_S = 1;
T = n * 2 + 2;
for (int i = 1; i <= n; ++ i) {
s[i].xl = read(), s[i].yl = read(), s[i].xr = read(), s[i].yr = read();
s[i].len = sqrt(1ll * (s[i].xr - s[i].xl) * (s[i].xr - s[i].xl) +
1ll * (s[i].yr - s[i].yl) * (s[i].yr - s[i].yl));
if (s[i].xr < s[i].xl) swap(s[i].xl, s[i].xr);
s[i].xr <<= 1, s[i].xl <<= 1;
(s[i].xr == s[i].xl) ? (s[i].xr |= 1) : (s[i].xl |= 1);
}
sort(s + 1, s + n + 1);
add(S, _S, k, 0);
for (int i = 1; i <= n; ++ i) {
add(i + 1, i + n + 1, 1, s[i].len);
add(i + n + 1, T, 1, 0);
add(_S, i + 1, 1, 0);
}
for (int i = 1; i <= n; ++ i) {
for (int j = n; j >= 1; -- j) {
if (i == j) continue;
if (s[i].xr <= s[j].xl) {
add(i + n + 1, j + 1, 1, 0);
}
else {
break;
}
}
}
while (spfa()) {
while (dfs(S, inf));
}
printf("%lld\n", cost);
return 0;
}
网络流做法总结
拆点法
拆边法
分类法
分层图
降维法
降维打击 最后一个题就是将二维图转化为了一维图,还有一些小方法,像扩域等