把网格最大流的有限最小割表示为每列分界高度,再用绝对值转移的双向扫描做 DP。
OJ: shumeng
题目 ID: CSP201703E
难度:提高+/省选-
标签:最大流最小割动态规划网格图优化
日期: 2026-07-31 16:21
形式化题目
给定一个
思路
在小网格上可以直接建立源点、汇点和每个格点,按管道方向连边后用 Dinic 求最大流。
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-07-31 16:21
* update_at: 2026-08-17 22:48
*/
// brute.cpp:小数据暴力解,建出完整网络后用 Dinic 求最大流。
#include <bits/stdc++.h>
using namespace std;
const long long INF = (1LL << 60);
struct Edge {
int to, reverse;
long long capacity;
};
struct Dinic {
vector<vector<Edge> > graph;
vector<int> level, current;
Dinic(int node_count) : graph(node_count), level(node_count), current(node_count) {}
void add_edge(int from, int to, long long capacity) {
Edge forward = {to, (int)graph[to].size(), capacity};
Edge backward = {from, (int)graph[from].size(), 0};
graph[from].push_back(forward);
graph[to].push_back(backward);
}
bool bfs(int source, int sink) {
fill(level.begin(), level.end(), -1);
queue<int> que;
level[source] = 0;
que.push(source);
while (!que.empty()) {
int x = que.front();
que.pop();
for (int i = 0; i < (int)graph[x].size(); i++) {
Edge &edge = graph[x][i];
if (edge.capacity > 0 && level[edge.to] == -1) {
level[edge.to] = level[x] + 1;
que.push(edge.to);
}
}
}
return level[sink] != -1;
}
long long dfs(int x, int sink, long long flow) {
if (x == sink) {
return flow;
}
for (int &i = current[x]; i < (int)graph[x].size(); i++) {
Edge &edge = graph[x][i];
if (edge.capacity == 0 || level[edge.to] != level[x] + 1) {
continue;
}
long long pushed = dfs(edge.to, sink, min(flow, edge.capacity));
if (pushed == 0) {
continue;
}
edge.capacity -= pushed;
graph[edge.to][edge.reverse].capacity += pushed;
return pushed;
}
return 0;
}
long long max_flow(int source, int sink) {
long long answer = 0;
while (bfs(source, sink)) {
fill(current.begin(), current.end(), 0);
while (true) {
long long pushed = dfs(source, sink, INF);
if (pushed == 0) {
break;
}
answer += pushed;
}
}
return answer;
}
};
long long next_value(long long &x, long long a, long long b, long long q) {
x = (a * x + b) % q;
return x;
}
int get_id(int row, int column, int m) {
return (row - 1) * m + column - 1;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
long long a, b, q, x;
cin >> n >> m >> a >> b >> q >> x;
int source = n * m;
int sink = source + 1;
Dinic dinic(sink + 1);
for (int column = 1; column <= m; column++) {
dinic.add_edge(source, get_id(1, column, m), INF);
dinic.add_edge(get_id(n, column, m), sink, INF);
}
for (int row = 1; row <= n - 1; row++) {
for (int column = 1; column <= m; column++) {
long long capacity = next_value(x, a, b, q);
int upper = get_id(row, column, m);
int lower = get_id(row + 1, column, m);
dinic.add_edge(upper, lower, capacity);
dinic.add_edge(lower, upper, INF);
}
}
for (int row = 2; row <= n - 1; row++) {
for (int column = 1; column <= m - 1; column++) {
long long capacity = next_value(x, a, b, q);
int left = get_id(row, column, m);
int right = get_id(row, column + 1, m);
dinic.add_edge(left, right, capacity);
dinic.add_edge(right, left, capacity);
}
}
cout << dinic.max_flow(source, sink) << '\n';
return 0;
}但正式网格有
最小割的形状
源点连着整条顶行、整条底行连着汇点,所以任意有限割中顶行必须在源点侧、底行必须在汇点侧。更关键的是每条竖管自下向上的容量无限:若某列中较低节点在源点侧而它上方节点在汇点侧,就会割到无限边。因此第
令
- 纵向代价是第
列第 条向下管道的容量,记为 。 - 相邻列的高度不同,会切断高度之间的横管。令
是第 与 列之间、从第 2 行累加到第 行的横管容量,约定 ,则横向代价为 。
DP 转移
设
跨过第
绝对值转移优化
直接枚举
从上到下维护第一式的前缀最小值、从下到上维护第二式的后缀最小值,即可在
样例分界高度 DP 表
表中
| 已处理列数 | ||
|---|---|---|
| 1 | 16 | 12 |
| 2 | 25 | 21 |
| 3 | 43 | 38 |
例如从第 1 列转到第 2 列时,横向前缀为
输入给出的容量按行生成,而 DP 按列访问。代码将竖管和横管容量分别转置保存为 int 数组,再逐列计算,避免构造任何流网络。
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-07-31 16:21
* update_at: 2026-08-17 22:48
*/
#include <bits/stdc++.h>
using namespace std;
const long long INF = (1LL << 62);
// 递推生成下一个伪随机数 X_{i+1} = (A*X_i + B) mod Q
long long next_value(long long &x, long long a, long long b, long long q) {
x = (a * x + b) % q;
return x;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
long long a, b, q, x;
cin >> n >> m >> a >> b >> q >> x;
// 容量按行生成、DP 按列使用,因此转置存储。
// vertical[column*n + row]:第 column 列、row 与 row+1 行之间的竖管容量。
// horizontal[column*n + row]:第 column 与 column+1 列之间、第 row 行的横管容量。
long long vertical_size = 1LL * (m + 1) * n;
long long horizontal_size = 1LL * m * n;
int *vertical = new int[vertical_size];
int *horizontal = new int[horizontal_size];
// 竖管:第 1..n-1 行、每列 m 条,对应数列的前 (n-1)m 项
for (int row = 1; row <= n - 1; row++) {
for (int column = 1; column <= m; column++) {
vertical[1LL * column * n + row] = (int)next_value(x, a, b, q);
}
}
// 横管:第 2..n-1 行、每行 m-1 条,对应数列的接下来 (n-2)(m-1) 项
for (int row = 2; row <= n - 1; row++) {
for (int column = 1; column <= m - 1; column++) {
horizontal[1LL * column * n + row] = (int)next_value(x, a, b, q);
}
}
// dp[h]:当前列在第 h 行与 h+1 行之间切断时的最小割代价。
// 第一列没有横向代价,初值就是本列竖管容量。
vector<long long> dp(n), next_dp(n), prefix(n);
for (int height = 1; height <= n - 1; height++) {
dp[height] = vertical[n + height];
}
for (int column = 2; column <= m; column++) {
// prefix[h]:第 column-1 与 column 列之间,从第 2 行累加到第 h 行的横管容量
prefix[1] = 0;
for (int height = 2; height <= n - 1; height++) {
prefix[height] = prefix[height - 1]
+ horizontal[1LL * (column - 1) * n + height];
}
// dp[col][b] = V[col][b] + min_a{ dp[col-1][a] + |P[a] - P[b]| }。
// 绝对值按 a<=b / a>=b 分成两半,各用一次前缀/后缀最小值完成转移。
long long best = INF;
for (int height = 1; height <= n - 1; height++) {
best = min(best, dp[height] - prefix[height]);
next_dp[height] = best + prefix[height];
}
best = INF;
for (int height = n - 1; height >= 1; height--) {
best = min(best, dp[height] + prefix[height]);
next_dp[height] = min(next_dp[height], best - prefix[height]);
next_dp[height] += vertical[1LL * column * n + height];
}
dp.swap(next_dp);
}
// 最后一列取所有高度的最小值
long long answer = INF;
for (int height = 1; height <= n - 1; height++) {
answer = min(answer, dp[height]);
}
cout << answer << '\n';
delete[] vertical;
delete[] horizontal;
return 0;
}复杂度
- 时间:容量生成和 DP 都遍历每条网格管道一次,时间复杂度为
。 - 空间:两个转置的容量数组使用
空间,最大规模下约为 200MB;另有 的 DP 数组。
总结
大网格最大流不一定要建图。先观察无限容量边如何限制有限最小割的形状,再把割边界参数化为一维状态;这里相邻状态的代价恰为单调前缀和之差,才能把二次转移降为双向扫描。
图示解析
这张图串起本题从管网到线性 DP 的主线:
并行施工的最大引水量
`- 最大流最小割
`- 向上放水边容量无穷大
`- 每列的有限割只能是“上方保留、下方切断”
|- 选择每列分界高度 h
|- 纵向代价:切一条竖管
`- 横向代价:相邻高度之间的横管前缀和之差
`- 左右扫描优化绝对值转移,O(nm)无穷大的反向竖边排除了分界线向下再回到源侧的形状,所以一个最小割可以完全由每列一个高度描述。 高度相邻时的费用是两个单调前缀和的绝对差,因而不必枚举全部前驱高度。

