【Coel.学习笔记】最大流的拆点与判定问题
最大流的问题好多……
最大流判定问题
这类问题通常会把二分、枚举、并查集等知识和最大流相结合。
[USACO2005FEB] Secret Milking Machine
洛谷没有收录(
在一个 \(N\) 点 \(P\) 边的无向图中从 \(1\) 到达 \(N\) 走 \(T\) 次,要求每次走过的道路互不相同,并让走过的最长道路最短化,求出这条道路的长度。
解析:“最大值小化”通常会考虑二分,先思考一下问题是否具有单调性。
很显然,对于一个二分中确定的值 \(x\),所有大于 \(x\) 的值都不存在而小于 \(x\) 的均可存在,因此满足二分性质。
由于走过的边有限制,所以考虑使用网络流模型。由于流网络中均为有向边,所以我们要把无向边建成双向。这时可能存在走两次的问题,把双向边删除即可,这样容量限制和流量守恒也不会受到影响。
那残留网络怎么办呢?建四条边当然没问题,但我们可以利用流量相加的原理,合并成两条边。
令起点为源点,终点为汇点,求最大流,那么如果最大流大于 \(T\) 则合法,反之不合法。
代码如下:
// Problem: 秘密挤奶机
// Contest: AcWing
// URL: https://www.acwing.com/problem/content/2279/
// Memory Limit: 64 MB
// Time Limit: 1000 ms
// Author: Coel
//
// Powered by CP Editor (https://cpeditor.org)
#include <iostream>
#include <cstring>
const int maxn = 1e5 + 10, inf = 1e8;
using namespace std;
int n, m, K, S, T;
int head[maxn], nxt[maxn], to[maxn], c[maxn], val[maxn], cnt;
int q[maxn], d[maxn], cur[maxn];
void add(int u, int v, int w) {
nxt[cnt] = head[u], to[cnt] = v, val[cnt] = w, head[u] = cnt++;
nxt[cnt] = head[v], to[cnt] = u, val[cnt] = w, head[v] = cnt++;
}
bool bfs() {
int hh = 0, tt = 0;
memset(d, -1, sizeof(d));
q[0] = S, d[S] = 0 , cur[S] = head[S];
while (hh <= tt) {
int u = q[hh++];
for (int i = head[u]; ~i; i = nxt[i]) {
int v = to[i];
if (d[v] == -1 && c[i]) {
d[v] = d[u] + 1;
cur[v] = head[v];
if (v == T) return true;
q[++tt] = v;
}
}
}
return false;
}
int find(int u, int limit) {
if (u == T) return limit;
int flow = 0;
for (int i = cur[u]; ~i && flow < limit; i = nxt[i]) {
cur[u] = i;
int v = to[i];
if (d[v] == d[u] + 1 && c[i]) {
int t = find(v, min(c[i], limit - flow));
if (!t) d[v] = -1;
c[i] -= t, c[i ^ 1] += t, flow += t;
}
}
return flow;
}
int dinic() {
int ans = 0, flow;
while (bfs())
while ((flow = find(S, inf)))
ans += flow;
return ans;
}
bool check(int mid) {
for (int i = 0; i < cnt; i++)
if (val[i] > mid) c[i] = 0; //删除不合法的边
else c[i] = 1;
return dinic() >= K; //判断最大流是否满足要求
}
int main(void) {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m >> K;
S = 1, T = n;
memset(head, -1, sizeof(head));
for (int i = 1; i <= m; i++) {
int u, v, w;
cin >> u >> v >> w;
add(u, v, w);
}
int l = 1, r = 1e6;
while (l < r) {
int mid = (l + r) >> 1;
if (check(mid)) r = mid;
else l = mid + 1;
}
cout << r;
return 0;
}
网络流 24 题:星际转移问题
洛谷传送门
现有 \(n\) 个太空站位于地球与月球之间,且有 \(m\) 艘公共交通太空船在其间来回穿梭。每个太空站可容纳无限多的人,而太空船的容量是有限的,第 \(i\) 艘太空船只可容纳 \(h_i\) 个人。每艘太空船将周期性地停靠一系列的太空站,例如 \((1,3,4)\) 表示该太空船将周期性地停靠太空站 \(134134134\dots\)。每一艘太空船从一个太空站驶往任一太空站耗时均为 \(1\)。人们只能在太空船停靠太空站(或月球、地球)时上、下船。
初始时所有人全在地球上,太空船全在初始站。试设计一个算法,找出让所有人尽快地全部转移到月球上的运输方案。
解析:先想想怎么判定有无解。无解就意味着起点与终点不连通,用并查集判断即可。
这时会有一个隐藏问题:并查集和 dinic 都有一个 find 函数。但不必担心,因为 C++ 有函数重定向,只要函参不同就可以区分函数了。当然也可以设置不同的函数名,或者开一个名字空间。
判断有解后,按照天数构造分层图。源点向第 \(0\) 层的第一个空间站连一条容量为人数的边,汇点向每一层的最后一个空间站连一条容量正无穷的边。然后对于每一个可行的运载,连一条容量等于太空船承载量的边。此外由于空间站中可以住人,所以不同天之间的相同空间站连容量无穷大的边。
枚举进行的天数,做一遍最大流。如果天数内最大流大于等于运输人数,那么方案合法。由于网络流可以在当前图上继续增广,所以正向枚举可以在上一次枚举的基础上继续求最大流,效率甚至能够高于每次都要建图的二分。
代码如下:
// Problem: P2754 [CTSC1999]家园 / 星际转移问题
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P2754
// Memory Limit: 125 MB
// Time Limit: 1000 ms
// Author: Coel
//
// Powered by CP Editor (https://cpeditor.org)
#include <cstdlib>
#include <cstring>
#include <iostream>
using namespace std;
const int maxn = 1e6 + 10, maxm = 1e5 + 10, inf = 1e8;
int n, m, k, S, T;
int head[maxn], nxt[maxn], to[maxn], c[maxn], cnt;
int q[maxn], d[maxn], cur[maxn];
int fa[30];
struct node {
int h, r, id[30];
} a[30];
int find(int x) {
return x == fa[x] ? x : fa[x] = find(fa[x]);
}
inline int get(int i, int day) {
return day * (n + 2) + i;
}
void add(int u, int v, int w) {
nxt[cnt] = head[u], to[cnt] = v, c[cnt] = w, head[u] = cnt++;
nxt[cnt] = head[v], to[cnt] = u, c[cnt] = 0, head[v] = cnt++;
}
bool bfs() {
int hh = 0, tt = 0;
memset(d, -1, sizeof(d));
q[0] = S, d[S] = 0, cur[S] = head[S];
while (hh <= tt) {
int u = q[hh++];
for (int i = head[u]; ~i; i = nxt[i]) {
int v = to[i];
if (d[v] == -1 && c[i]) {
d[v] = d[u] + 1;
cur[v] = head[v];
if (v == T) return true;
q[++tt] = v;
}
}
}
return false;
}
int find(int u, int limit) {
if (u == T) return limit;
int flow = 0;
for (int i = cur[u]; ~i && flow < limit; i = nxt[i]) {
cur[u] = i;
int v = to[i];
if (d[v] == d[u] + 1 && c[i]) {
int t = find(v, min(c[i], limit - flow));
if (!t) d[v] = -1;
c[i] -= t, c[i ^ 1] += t, flow += t;
}
}
return flow;
}
int dinic() {
int ans = 0, flow;
while (bfs())
while ((flow = find(S, inf)))
ans += flow;
return ans;
}
int main(void) {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m >> k;
S = maxm - 2, T = maxm - 1;
memset(head, -1, sizeof(head));
for (int i = 0; i < 30; i++)
fa[i] = i;
for (int i = 1; i <= m; i++) {
int h, r;
cin >> h >> r;
a[i] = {h, r};
for (int j = 0; j < r; j++) {
int id;
cin >> id;
if (id == -1) id = n + 1;
a[i].id[j] = id;
if (j) {
int x = a[i].id[j - 1];
fa[find(x)] = find(id);
}
}
}
if (find(0) != find(n + 1))
cout << 0, exit(0);
add(S, get(0, 0), k);
add(get(n + 1, 0), T, inf);
int day = 1, res = 0;
while (true) {
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 r = a[i].r;
int u = a[i].id[(day - 1) % r], v = a[i].id[day % r];
add(get(u, day - 1), get(v, day), a[i].h);
}
res += dinic();
if (res >= k) break;
day++;
}
cout << day;
return 0;
}
最大流拆点问题
拆点可以解决很多变式最大流问题。
[USACO07OPEN]Dining G
洛谷传送门
约翰一共烹制了 \(F\) 种食物,并提供了 \(D\) 种饮料,每种食物和饮料都只有一份。
有 \(N\) 头奶牛,其中第 \(i\) 头奶牛有 \(F_i\) 种喜欢的食物以及 \(D_i\) 种喜欢的饮料。
约翰需要给每头奶牛分配一种食物和一种饮料,并使得有吃有喝的奶牛数量尽可能大。
解析:这题看起来有点像二分图匹配,但实际上有三排点:食物,饮料,奶牛。
一种可能想到的方法为建立源点与食物相连,汇点与饮料相连,奶牛和喜欢的食物与饮料相连。但这时不能保证每一头奶牛都只对应一份食物与饮料,所以要改变方法。
使用拆点的方法,把奶牛拆成入点与出点,且入点与出点连容量为 \(1\) 的边,这样就可以保证解的正确性了。
代码如下:
// Problem: P2891 [USACO07OPEN]Dining G
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P2891
// Memory Limit: 125 MB
// Time Limit: 1000 ms
// Author: Coel
//
// Powered by CP Editor (https://cpeditor.org)
#include <cstring>
#include <iostream>
using namespace std;
const int maxn = 5e4 + 10, inf = 1e8;
int n, F, D, S, T;
int head[maxn], nxt[maxn], to[maxn], c[maxn], cnt;
int q[maxn], d[maxn], cur[maxn];
void add(int u, int v, int w) {
nxt[cnt] = head[u], to[cnt] = v, c[cnt] = w, head[u] = cnt++;
nxt[cnt] = head[v], to[cnt] = u, c[cnt] = 0, head[v] = cnt++;
}
bool bfs() {
int hh = 0, tt = 0;
memset(d, -1, sizeof(d));
q[0] = S, d[S] = 0, cur[S] = head[S];
while (hh <= tt) {
int u = q[hh++];
for (int i = head[u]; ~i; i = nxt[i]) {
int v = to[i];
if (d[v] == -1 && c[i]) {
d[v] = d[u] + 1;
cur[v] = head[v];
if (v == T) return true;
q[++tt] = v;
}
}
}
return false;
}
int find(int u, int limit) {
if (u == T) return limit;
int flow = 0;
for (int i = cur[u]; ~i && flow < limit; i = nxt[i]) {
cur[u] = i;
int v = to[i];
if (d[v] == d[u] + 1 && c[i]) {
int t = find(v, min(c[i], limit - flow));
if (!t) d[v] = -1;
c[i] -= t, c[i ^ 1] += t, flow += t;
}
}
return flow;
}
int dinic() {
int res = 0, flow;
while (bfs())
while ((flow = find(S, inf)))
res += flow;
return res;
}
int main(void) {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> F >> D;
S = 0, T = 2 * n + F + D + 1;
memset(head, -1, sizeof(head));
for (int i = 1; i <= F; i++)
add(S, n * 2 + i, 1);
for (int i = 1; i <= D; i++)
add(n * 2 + F + i, T, 1);
for (int i = 1; i <= n; i++) {
add(i, n + i, 1);
int x, y;
cin >> x >> y;
for (int j = 1; j <= x; j++) {
int t;
cin >> t;
add(n * 2 + t, i, 1);
}
for (int j = 1; j <= y; j++) {
int t;
cin >> t;
add(i + n, n * 2 + F + t, 1);
}
}
cout << dinic();
return 0;
}
网络流 24 题:最长不下降子序列问题
洛谷传送门
给定正整数序列 \(x_1 \ldots, x_n\)。
- 计算其最长不下降子序列的长度 \(s\)。
- 如果每个元素只允许使用一次,计算从给定的序列中最多可取出多少个长度为 \(s\) 的不下降子序列。
- 如果允许在取出的序列中多次使用 \(x_1\) 和 \(x_n\)(其他元素仍然只允许使用一次),则从给定序列中最多可取出多少个不同的长度为 \(s\) 的不下降子序列。
令 \(a_1, a_2, \ldots, a_s\) 为构造 \(S\) 时所使用的下标,\(b_1, b_2, \ldots, b_s\) 为构造 \(T\) 时所使用的下标。且 \(\forall i \in [1,s-1]\),都有 \(a_i \lt a_{i+1}\),\(b_i \lt b_{i+1}\)。则 \(S\) 和 \(T\) 不同,当且仅当 \(\exists i \in [1,s]\),使得 \(a_i \neq b_i\)。
解析:第一问就是一个简单的动态规划问题,略过。
对于第二问,要在第一问的基础上思考。假设 \(f_i\) 可以从 \(f_j\) 转移而来,那么我们给 \(i,j\) 连边,这样就得到了一张有向图。这时,长度为 \(n\) 的路径可以一一对应上长度为 \(n\) 的 LIS。点有限制,故用拆点法。
对于第三问,把限制用的边改成正无穷即可。
代码如下:
// Problem: P2766 最长不下降子序列问题
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P2766
// Memory Limit: 125 MB
// Time Limit: 1000 ms
// Author: Coel
//
// Powered by CP Editor (https://cpeditor.org)
#include <cstring>
#include <iostream>
const int maxn = 5e5 + 10, inf = 1e8;
using namespace std;
int n, S, T;
int head[maxn], nxt[maxn], to[maxn], c[maxn], cnt;
int q[maxn], d[maxn], cur[maxn];
int dp[maxn], w[maxn];
void add(int u, int v, int w) {
nxt[cnt] = head[u], to[cnt] = v, c[cnt] = w, head[u] = cnt++;
nxt[cnt] = head[v], to[cnt] = u, c[cnt] = 0, head[v] = cnt++;
}
bool bfs() {
int hh = 0, tt = 0;
memset(d, -1, sizeof(d));
q[0] = S, d[S] = 0, cur[S] = head[S];
while (hh <= tt) {
int u = q[hh++];
for (int i = head[u]; ~i; i = nxt[i]) {
int v = to[i];
if (d[v] == -1 && c[i]) {
d[v] = d[u] + 1;
cur[v] = head[v];
if (v == T) return true;
q[++tt] = v;
}
}
}
return false;
}
int find(int u, int limit) {
if (u == T) return limit;
int flow = 0;
for (int i = cur[u]; ~i && flow < limit; i = nxt[i]) {
cur[u] = i;
int v = to[i];
if (d[v] == d[u] + 1 && c[i]) {
int t = find(v, min(c[i], limit - flow));
if (!t) d[v] = -1;
c[i] -= t, c[i ^ 1] += t, flow += t;
}
}
return flow;
}
int dinic() {
int res = 0, flow;
while (bfs())
while ((flow = find(S, inf)))
res += flow;
return res;
}
int main(void) {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
S = 0, T = n * 2 + 1;
memset(head, -1, sizeof(head));
for (int i = 1; i <= n; i++)
cin >> w[i];
int s = 0;
for (int i = 1; i <= n; i++) {
add(i, i + n, 1);
dp[i] = 1;
for (int j = 1; j < i; j++)
if (w[j] <= w[i])
dp[i] = max(dp[i], dp[j] + 1);
for (int j = 1; j < i; j++)
if (w[j] <= w[i] && dp[j] + 1 == dp[i])
add(n + j, i, 1);
s = max(s, dp[i]);
if (dp[i] == 1) add(S, i, 1);
}
for (int i = 1; i <= n; i++)
if (dp[i] == s) add(n + i, T, 1);
cout << s << '\n';
if (s == 1) cout << n << '\n' << n << '\n', exit(0);
int res = dinic();
cout << res << '\n';
for (int i = 0; i < cnt; i += 2) {
int u = to[i ^ 1], v = to[i];
if (u == S && v == 1) c[i] = inf;
if (u == 1 && v == n + 1) c[i] = inf;
if (u == n && v == n + n) c[i] = inf;
if (u == n + n && v == T) c[i] = inf;
}
cout << res + dinic();
return 0;
}
[POJ3498] March of the Penguins 企鹅游行
给定 \(n\) 块冰的坐标和企鹅能跳的距离 \(d\),每块冰有 \(4\) 个属性,分别为 \(x\) 坐标,\(y\) 坐标,上面原有的企鹅的数量和最多能跳出多少次,求哪些冰块可以让所有企鹅都跳到上面。
解析:到达网络流第一道黑题,认真起来吧!
先想想怎么把这题转化为流网络。建立一个源点与所有浮冰相连,容量等于浮冰起始的企鹅数。然后对于每个可以跳到的浮冰,都可以连一条边。汇点不固定,但数据范围很小 (\(1\leq N \leq 100\)),可以直接枚举每个浮冰作为汇点。
再想想怎么解决起跳次数限制的问题。根据经验,点有限制时我们可以拆点解决。对于某个点 \(u\),把这个点拆成入点 \(u_1\) 和出点 \(u_2\),然后在这两点中间连一条边,容量等于最大起跳次数。
此时,这道题就转化成枚举汇点并判断最大流是否满流,如果满流,就意味着所选择的汇点可以满足要求。
另外在枚举汇点时要注意一个小问题:每次求完最大流之后要把网络还原。我们利用残留网络的流量定义(正向边等于可以增加的流量,反向边等于可以返回的流量)来还原,也就是给正向边加上可行流,反向边设为 \(0\)。
由于这题的变量很多,所以在定义变量的时候一定要明确每个变量的含义,并且防止变量重名。
代码如下:
// Problem: UVA12125 March of the Penguins
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/UVA12125
// Memory Limit: 0 MB (Are you sure?)
// Time Limit: 3000 ms
// Author: Coel
//
// Powered by CP Editor (https://cpeditor.org)
#include <cctype>
#include <cmath>
#include <cstring>
#include <iostream>
#include <queue>
using namespace std;
const int maxn = 3e4 + 10, inf = 1e8;
const double eps = 1e-8; // 考虑浮点误差
int n, S, T;
int head[maxn], nxt[maxn], to[maxn], c[maxn], cnt;
int d[maxn], cur[maxn];
double D;
struct node {
int x, y;
} a[maxn];
double dis(node a, node b) {
double x = a.x - b.x, y = a.y - b.y;
return sqrt(x * x + y * y);
}
void add(int u, int v, int w) {
nxt[cnt] = head[u], to[cnt] = v, c[cnt] = w, head[u] = cnt++;
nxt[cnt] = head[v], to[cnt] = u, c[cnt] = 0, head[v] = cnt++;
}
bool bfs() {
queue<int> Q;
memset(d, -1, sizeof(d));
Q.push(S), d[S] = 0, cur[S] = head[S];
while (!Q.empty()) {
int u = Q.front();
Q.pop();
for (int i = head[u]; ~i; i = nxt[i]) {
int v = to[i];
if (d[v] == -1 && c[i]) {
d[v] = d[u] + 1;
cur[v] = head[v];
if (v == T) return true;
Q.push(v);
}
}
}
return false;
}
int find(int u, int limit) {
if (u == T) return limit;
int flow = 0;
for (int i = cur[u]; ~i && flow < limit; i = nxt[i]) {
cur[u] = i;
int v = to[i];
if (d[v] == d[u] + 1 && c[i]) {
int t = find(v, min(c[i], limit - flow));
if (!t) d[v] = -1;
c[i] -= t, c[i ^ 1] += t, flow += t;
}
}
return flow;
}
int dinic() {
int res = 0, flow;
while (bfs())
while ((flow = find(S, inf)))
res += flow;
return res;
}
int main(void) {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int Tt;
cin >> Tt; // Tt 为数据组数, T 为汇点
while (Tt--) {
memset(head, -1, sizeof(head));
int tot = 0, Cnt = 0; // Cnt:企鹅总数 tot:可以跳到的浮冰总数
cnt = S = 0; //小写的 cnt 用于链式前向星
cin >> n >> D;
for (int i = 1; i <= n; i++) {
int num, lim; //num 为该点企鹅数,lim 为最大跳出次数
cin >> a[i].x >> a[i].y >> num >> lim;
add(S, i, num), add(i, n + i, lim);
Cnt += num;
}
for (int i = 1; i <= n; i++)
for (int j = i + 1; j <= n; j++)
if (dis(a[i], a[j]) < D + eps)
add(n + i, j, inf), add(n + j, i, inf);
for (T = 1; T <= n; T++) {
for (int i = 0; i < cnt; i += 2)
c[i] += c[i ^ 1], c[i ^ 1] = 0; //还原网络
if (dinic() == Cnt) {
if (tot != 0) cout << ' ';
cout << T - 1; //原题中浮冰编号从 0 开始,所以要 -1
tot++;
}
}
if (!tot) cout << -1;
cout << '\n';
}
return 0;
}