[CSP-S 2021] 交通规划

GitHub跳转原题关系图返回列表

把平面图最小割转成对偶图最短路,再对边界颜色变化点做环形区间 DP。

OJ: luogu

题目 ID: P7916

难度:提高+/省选-

标签:图论最小割对偶图最短路动态规划

日期: 2026-07-06 08:46

题意

给定一个 n×mn \times m 的格点网格,相邻格点之间有边权。每次询问会在网格边界外添加若干个已知颜色的附加点,并给出附加点到最近格点的边权。

需要把所有网格内格点染成黑色或白色,使得所有两端颜色不同的边权和最小。

思路

小网格可以直接枚举每个格点的颜色,然后计算所有跨色边代价:

cpp
// brute.cpp:小网格暴力解,枚举每个格点染黑/白,直接计算割边代价。
#include <bits/stdc++.h>
using namespace std;

struct BaseEdge {
    int u;
    int v;
    int w;
};

struct ExtraEdge {
    int u;
    int w;
    int color;
};

int n, m, T;
vector<BaseEdge> base_edges;

int node_id(int r, int c) {
    return (r - 1) * m + c - 1;
}

int boundary_node(int p) {
    if (p <= m) {
        return node_id(1, p);
    }
    p -= m;
    if (p <= n) {
        return node_id(p, m);
    }
    p -= n;
    if (p <= m) {
        return node_id(n, m - p + 1);
    }
    p -= m;
    return node_id(n - p + 1, 1);
}

long long solve_query() {
    int k;
    cin >> k;
    vector<ExtraEdge> extra_edges;
    for (int i = 1; i <= k; i++) {
        int x, p, color;
        cin >> x >> p >> color;
        ExtraEdge e;
        e.u = boundary_node(p);
        e.w = x;
        e.color = color;
        extra_edges.push_back(e);
    }

    int total = n * m;
    long long answer = (long long)4e18;
    for (int mask = 0; mask < (1 << total); mask++) {
        long long cost = 0;
        for (int i = 0; i < (int)base_edges.size(); i++) {
            int cu = (mask >> base_edges[i].u) & 1;
            int cv = (mask >> base_edges[i].v) & 1;
            if (cu != cv) {
                cost += base_edges[i].w;
            }
        }
        for (int i = 0; i < (int)extra_edges.size(); i++) {
            int cu = (mask >> extra_edges[i].u) & 1;
            if (cu != extra_edges[i].color) {
                cost += extra_edges[i].w;
            }
        }
        answer = min(answer, cost);
    }
    return answer;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> m >> T;
    for (int r = 1; r < n; r++) {
        for (int c = 1; c <= m; c++) {
            int w;
            cin >> w;
            BaseEdge e;
            e.u = node_id(r, c);
            e.v = node_id(r + 1, c);
            e.w = w;
            base_edges.push_back(e);
        }
    }
    for (int r = 1; r <= n; r++) {
        for (int c = 1; c < m; c++) {
            int w;
            cin >> w;
            BaseEdge e;
            e.u = node_id(r, c);
            e.v = node_id(r, c + 1);
            e.w = w;
            base_edges.push_back(e);
        }
    }

    while (T--) {
        cout << solve_query() << '\n';
    }

    return 0;
}

这个暴力的状态数是 2n×m2^{n \times m},只能用于验证建模。

这类“每个点二选一,边在两端选择不同时产生代价”的问题,首先可以看成源汇最小割模型:

  • 源点 S 表示黑色;
  • 汇点 T 表示白色;
  • 每个格点是图中的一个点;
  • 网格内部一条权值为 w 的无向边 (u,v),表示 u,v 颜色不同就付出 w,在网络流中建成容量为 w 的双向割边;
  • 如果附加点颜色为黑色,且它连到格点 u、边权为 w,那么 u 被染成白色时要付出 w,所以连边 S -> u,容量为 w
  • 如果附加点颜色为白色,则连边 u -> T,容量为 w

一次 S-T 割会把所有格点分到源点侧或汇点侧,正好对应黑白染色。割掉的边容量之和,正好是所有跨色边代价。因此最小割就是答案。

但直接跑最大流会太慢。网格是平面图,平面图中的最小割可以转成对偶图上的最短路:原图里割掉一条边,对偶图中就相当于走过这条边对应的对偶边。

对于一次询问,把所有附加点按边界射线编号排成环。相邻两个附加点之间的一段边界属于同一个“外部区域”,代码为每段边界建立一个对偶点,并用权值为附加边权的边连接相邻外部区域。

如果相邻两个附加点颜色不同,那么这两个外部区域之间必须被某条割线分开。于是我们只关心所有“颜色发生变化的位置”。这些位置在边界上成环出现,割线之间不能交叉,所以可以用环形区间 DP 配对它们:

text
dp[l][r]:把边界变化点 l..r 这段全部配对的最小代价

转移有两类:

  • llrr 配成一对,中间继续配对;
  • 把区间拆成两段分别配对。

任意两个变化点之间的配对代价,就是对偶图上这两个外部区域之间的最短路。代码对每个变化点跑一次 Dijkstra,得到这些代价。

代码

cpp
// main.cpp:平面图最小割转对偶图最短路,再用环形区间 DP 配对颜色变化段。
#include <bits/stdc++.h>
using namespace std;

const int MAXNODE = 260000;
const int MAXEDGE = 1300000;
const long long INF = (long long)4e18;

struct Edge {
    int to;
    int next;
    int weight;
};

struct PointInfo {
    int weight;
    int position;
    int color;
};

int n, m, query_count, face_count, edge_count;
int head[MAXNODE], saved_head[MAXEDGE], saved_node[MAXEDGE], saved_count;
Edge edges[MAXEDGE];
int boundary_face[5005];
PointInfo point_info[60];
long long dist_value[MAXNODE];
bool visited_node[MAXNODE];
long long dist_between[60][60];
long long dp[120][120];
int changed_node[120];

int face_id(int x, int y) {
    return x * (m + 1) + y;
}

bool cmp_point(const PointInfo &a, const PointInfo &b) {
    return a.position < b.position;
}

void add_directed_edge(int u, int v, int w) {
    edge_count++;
    edges[edge_count].to = v;
    edges[edge_count].weight = w;
    edges[edge_count].next = head[u];
    head[u] = edge_count;
}

void add_base_edge(int u, int v, int w) {
    add_directed_edge(u, v, w);
    add_directed_edge(v, u, w);
}

void add_temp_edge(int u, int v, int w) {
    saved_count++;
    saved_head[saved_count] = head[u];
    saved_node[saved_count] = u;
    add_directed_edge(u, v, w);
}

void reset_temp_edges() {
    edge_count -= saved_count;
    while (saved_count > 0) {
        head[saved_node[saved_count]] = saved_head[saved_count];
        saved_count--;
    }
}

void dijkstra(int source, int total_nodes) {
    for (int i = 0; i < total_nodes; i++) {
        dist_value[i] = INF;
        visited_node[i] = false;
    }

    priority_queue<pair<long long, int>, vector<pair<long long, int> >, greater<pair<long long, int> > > heap;
    dist_value[source] = 0;
    heap.push(make_pair(0, source));

    while (!heap.empty()) {
        int u = heap.top().second;
        heap.pop();
        if (visited_node[u]) {
            continue;
        }
        visited_node[u] = true;

        for (int e = head[u]; e != 0; e = edges[e].next) {
            int v = edges[e].to;
            long long nd = dist_value[u] + edges[e].weight;
            if (nd < dist_value[v]) {
                dist_value[v] = nd;
                heap.push(make_pair(nd, v));
            }
        }
    }
}

void solve_query() {
    int k;
    cin >> k;
    for (int i = 1; i <= k; i++) {
        cin >> point_info[i].weight >> point_info[i].position >> point_info[i].color;
    }
    sort(point_info + 1, point_info + k + 1, cmp_point);
    point_info[k + 1] = point_info[1];

    int perimeter = 2 * n + 2 * m;
    vector<int> boundary_blocks;

    for (int i = 1; i <= k; i++) {
        int block_node = face_count + i - 1;

        for (int p = point_info[i].position; p != point_info[i + 1].position; p = p % perimeter + 1) {
            add_temp_edge(block_node, boundary_face[p], 0);
            add_temp_edge(boundary_face[p], block_node, 0);
        }

        int next_block = (i == k) ? face_count : block_node + 1;
        int connect_weight = (i == k) ? point_info[1].weight : point_info[i + 1].weight;
        add_temp_edge(block_node, next_block, connect_weight);
        add_temp_edge(next_block, block_node, connect_weight);

        if (point_info[i].color != point_info[i + 1].color) {
            boundary_blocks.push_back(block_node);
        }
    }

    int change_count = (int)boundary_blocks.size();
    if (change_count < 2) {
        cout << 0 << '\n';
        reset_temp_edges();
        return;
    }

    int total_nodes = face_count + k;
    for (int i = 0; i < change_count; i++) {
        dijkstra(boundary_blocks[i], total_nodes);
        for (int j = 0; j < change_count; j++) {
            dist_between[i][j] = dist_value[boundary_blocks[j]];
        }
    }

    for (int i = 0; i < change_count; i++) {
        changed_node[i] = i;
        changed_node[i + change_count] = i;
    }

    for (int i = 0; i < change_count * 2; i++) {
        for (int j = 0; j < change_count * 2; j++) {
            dp[i][j] = INF;
        }
    }

    for (int i = 0; i + 1 < change_count * 2; i++) {
        dp[i][i + 1] = dist_between[changed_node[i]][changed_node[i + 1]];
    }

    for (int len = 4; len <= change_count; len += 2) {
        for (int l = 0; l + len - 1 < change_count * 2; l++) {
            int r = l + len - 1;
            dp[l][r] = dp[l + 1][r - 1] + dist_between[changed_node[l]][changed_node[r]];
            for (int mid = l + 1; mid <= r - 2; mid += 2) {
                dp[l][r] = min(dp[l][r], dp[l][mid] + dp[mid + 1][r]);
            }
        }
    }

    long long answer = INF;
    for (int start = 0; start < change_count; start++) {
        answer = min(answer, dp[start][start + change_count - 1]);
    }
    cout << answer << '\n';

    reset_temp_edges();
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> m >> query_count;
    face_count = (n + 1) * (m + 1);

    for (int r = 1; r < n; r++) {
        for (int c = 1; c <= m; c++) {
            int w;
            cin >> w;
            add_base_edge(face_id(r, c - 1), face_id(r, c), w);
        }
    }
    for (int r = 1; r <= n; r++) {
        for (int c = 1; c < m; c++) {
            int w;
            cin >> w;
            add_base_edge(face_id(r - 1, c), face_id(r, c), w);
        }
    }

    for (int i = 1; i <= m; i++) {
        boundary_face[i] = face_id(0, i);
    }
    for (int i = m + 1; i <= n + m; i++) {
        boundary_face[i] = face_id(i - m, m);
    }
    for (int i = n + m + 1; i <= n + 2 * m; i++) {
        boundary_face[i] = face_id(n, n + 2 * m - i);
    }
    for (int i = n + 2 * m + 1; i <= 2 * n + 2 * m; i++) {
        boundary_face[i] = face_id(2 * n + 2 * m - i, 0);
    }

    while (query_count--) {
        solve_query();
    }

    return 0;
}

复杂度

设对偶图点数、边数都是 O(nm)O(nm)。一次询问中颜色变化点数量不超过 k <= 50

每个变化点跑一次 Dijkstra,复杂度为 O(knmlog(nm))O(k \cdot nm \log(nm))。区间 DP 的复杂度为 O(k3)O(k^3)

因为所有询问的 kk 总和不超过 50,整体可以通过。

总结

本题的关键是两次转化:

  1. 染黑/染白对应源汇两侧,跨色边代价对应最小割;
  2. 网格是平面图,最小割可以在对偶图上变成最短路。

最后利用边界颜色变化点数量很小,把多条割线的不交叉配对交给区间 DP 处理。