引水入城

把网格最大流的有限最小割表示为每列分界高度,再用绝对值转移的双向扫描做 DP。

OJ: shumeng

题目 ID: CSP201703E

难度:提高+/省选-

标签:最大流最小割动态规划网格图优化

日期: 2026-07-31 16:21

形式化题目

给定一个 n×mn \times m 的管道网络。第 1 行节点是水源(容量无限取水),第 nn 行节点连向城市。纵向管道向下抽水有给定容量、向上放水容量无限;中间各行的横向管道双向抽水、容量给定。求单位时间从顶行送往底行的最大水量。

思路

在小网格上可以直接建立源点、汇点和每个格点,按管道方向连边后用 Dinic 求最大流。

cpp
/**
 * 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;
}

但正式网格有 5000×50005000 \times 5000 个节点,不能建完整网络。改从最大流最小割看结构。

最小割的形状

源点连着整条顶行、整条底行连着汇点,所以任意有限割中顶行必须在源点侧、底行必须在汇点侧。更关键的是每条竖管自下向上的容量无限:若某列中较低节点在源点侧而它上方节点在汇点侧,就会割到无限边。因此第 jj 列的源点侧只能是一个前缀。

hj (1hj<n)h_j \ (1 \le h_j < n) 表示第 jj 列在第 hjh_j 行和第 hj+1h_j+1 行之间切断。这样一个有限割完全由 mm 个高度描述:

  • 纵向代价是第 jj 列第 hjh_j 条向下管道的容量,记为 Vj[hj]V_j[h_j]
  • 相邻列的高度不同,会切断高度之间的横管。令 Pj[h]P_j[h] 是第 jjj+1j+1 列之间、从第 2 行累加到第 hh 行的横管容量,约定 Pj[1]=0P_j[1]=0,则横向代价为 Pj[hj]Pj[hj+1]|P_j[h_j]-P_j[h_{j+1}]|

DP 转移

dp[j][h]dp[j][h] 为处理完前 jj 列且第 jj 列高度为 hh 的最小割。初值为:

dp[1][h]=V1[h] dp[1][h]=V_1[h]

跨过第 j1j-1 条列间边时:

dp[j][b]=Vj[b]+mina{dp[j1][a]+Pj1[a]Pj1[b]} dp[j][b]=V_j[b]+\min_a\{dp[j-1][a]+|P_{j-1}[a]-P_{j-1}[b]|\}

绝对值转移优化

直接枚举 a,ba, bO(n2m)O(n^2 m)。由于 PP 随高度单调不减,对固定 bb 把绝对值拆成两半:

ab:P[b]+minab(dp[a]P[a])ab:P[b]+minab(dp[a]+P[a]) \begin{aligned} a\le b &: P[b]+\min_{a\le b}(dp[a]-P[a])\\ a\ge b &: -P[b]+\min_{a\ge b}(dp[a]+P[a]) \end{aligned}

从上到下维护第一式的前缀最小值、从下到上维护第二式的后缀最小值,即可在 O(n)O(n) 时间完成一列转移。

样例分界高度 DP 表

表中 dp[j][h]dp[j][h] 表示前 jj 列的最小割代价,且第 jj 列在第 hh 行与第 h+1h+1 行之间切断。

已处理列数 h=1h=1 h=2h=2
1 16 12
2 25 21
3 43 38

例如从第 1 列转到第 2 列时,横向前缀为 (0,2)(0, 2)dp[2][1]=11+min(16+0,12+2)=25dp[2][1]=11+\min(16+0, 12+2)=25dp[2][2]=9+min(16+2,12+0)=21dp[2][2]=9+\min(16+2, 12+0)=21。最后取最后一列所有高度的最小值,得到样例答案 3838

输入给出的容量按行生成,而 DP 按列访问。代码将竖管和横管容量分别转置保存为 int 数组,再逐列计算,避免构造任何流网络。

代码

cpp
/**
 * 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 都遍历每条网格管道一次,时间复杂度为 O(nm)O(nm)
  • 空间:两个转置的容量数组使用 O(nm)O(nm) 空间,最大规模下约为 200MB;另有 O(n)O(n) 的 DP 数组。

总结

大网格最大流不一定要建图。先观察无限容量边如何限制有限最小割的形状,再把割边界参数化为一维状态;这里相邻状态的代价恰为单调前缀和之差,才能把二次转移降为双向扫描。

图示解析

这张图串起本题从管网到线性 DP 的主线:

text
并行施工的最大引水量
`- 最大流最小割
   `- 向上放水边容量无穷大
      `- 每列的有限割只能是“上方保留、下方切断”
         |- 选择每列分界高度 h
         |- 纵向代价:切一条竖管
         `- 横向代价:相邻高度之间的横管前缀和之差
            `- 左右扫描优化绝对值转移,O(nm)

无穷大的反向竖边排除了分界线向下再回到源侧的形状,所以一个最小割可以完全由每列一个高度描述。 高度相邻时的费用是两个单调前缀和的绝对差,因而不必枚举全部前驱高度。