[CSP-S 2025] 道路修复

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

枚举被城市化的乡镇集合,并用原图 MST 替换性质把每次 Kruskal 的原图边压缩到 n-1 条。

OJ: luogu

题目 ID: P14362

难度:提高+/省选-

标签:图论最小生成树枚举并查集

日期: 2026-06-22 19:46

题意

n 座原有城市和 m 条原有城市之间的双向道路。所有道路都坏了,修复第 i 条道路费用为 w_i

另外有 k 个乡镇,k <= 10。可以选择任意多个乡镇进行城市化改造。选择第 j 个乡镇要先支付 c_j,之后可以建造它到任意原有城市的道路,连接到第 i 座城市的费用为 a[j][i]

要求让原有的 n 座城市两两连通,求最小总费用。

思路

先看一个可以直接验证想法的朴素解:

cpp
// brute.cpp:小数据暴力解,用来帮助理解题意并辅助对拍。
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 20;
const int MAXK = 5;
const long long INF = (1LL << 62);

struct Edge {
    int u;
    int v;
    long long w;
};

int n, m, k;
long long city_cost[MAXK];
long long a[MAXK][MAXN];
vector<Edge> original_edges;

int fa[MAXN + MAXK], sz[MAXN + MAXK];

bool cmp_edge(const Edge &x, const Edge &y) {
    return x.w < y.w;
}

void init_dsu(int total) {
    for (int i = 1; i <= total; i++) {
        fa[i] = i;
        sz[i] = 1;
    }
}

int find_set(int x) {
    while (fa[x] != x) {
        fa[x] = fa[fa[x]];
        x = fa[x];
    }
    return x;
}

bool unite_set(int x, int y) {
    int fx = find_set(x);
    int fy = find_set(y);
    if (fx == fy) {
        return false;
    }
    if (sz[fx] < sz[fy]) {
        swap(fx, fy);
    }
    fa[fy] = fx;
    sz[fx] += sz[fy];
    return true;
}

long long solve_mask(int mask) {
    vector<Edge> edges = original_edges;
    long long cost = 0;
    int selected_towns = 0;

    for (int j = 0; j < k; j++) {
        if ((mask & (1 << j)) == 0) {
            continue;
        }
        selected_towns++;
        cost += city_cost[j];
        for (int i = 1; i <= n; i++) {
            Edge e;
            e.u = i;
            e.v = n + j + 1;
            e.w = a[j][i];
            edges.push_back(e);
        }
    }

    sort(edges.begin(), edges.end(), cmp_edge);
    init_dsu(n + k);

    int need_edges = n + selected_towns - 1;
    int picked = 0;
    for (int i = 0; i < (int)edges.size() && picked < need_edges; i++) {
        if (unite_set(edges[i].u, edges[i].v)) {
            cost += edges[i].w;
            picked++;
        }
    }

    if (picked < need_edges) {
        return INF;
    }
    return cost;
}

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

    cin >> n >> m >> k;
    original_edges.clear();
    for (int i = 1; i <= m; i++) {
        Edge e;
        cin >> e.u >> e.v >> e.w;
        original_edges.push_back(e);
    }

    for (int j = 0; j < k; j++) {
        cin >> city_cost[j];
        for (int i = 1; i <= n; i++) {
            cin >> a[j][i];
        }
    }

    long long ans = INF;
    for (int mask = 0; mask < (1 << k); mask++) {
        ans = min(ans, solve_mask(mask));
    }

    cout << ans << '\n';
    return 0;
}

因为 k <= 10,可以枚举每个乡镇是否城市化。固定一个乡镇集合后,把原有城市和这些乡镇放在同一张图里,边包括:

  • 原有城市之间的 m 条修复边;
  • 被选中乡镇到每个原有城市的建造边。

这时最小连接费用就是这张扩展图的最小生成树费用,再加上选中乡镇的改造费用。

直接这么做的问题在于 m 最大有 10^6,如果每个乡镇集合都重新处理所有原图边,会非常慢。

关键优化:原有城市之间的边,只需要保留原图的一棵 MST。

先在原有 n 座城市和 m 条道路上求出一棵 MST,记为 T。对于任意不在 T 里的原图边 e=(u,v,w)Tuv 的路径上不会有比 w 更大的边。否则可以用 e 替换那条更大的边,得到更小的生成树,矛盾。

因此,如果某个固定乡镇集合的方案用了非 MST 原图边 e,就可以用 T 上连接 u,v 的路径中的边替换它,费用不会增加。乡镇节点只连接原有城市,不会破坏这个替换过程。

所以固定乡镇集合时,只需要在:

text
原图 MST 的 n-1 条边 + 被选中乡镇到城市的边

上跑 Kruskal。

实现时,把所有乡镇到城市的边统一排序。枚举 mask 表示选中的乡镇集合,每次用两个指针归并扫描:

  • 原图 MST 边;
  • 当前 mask 允许的乡镇边。

按边权从小到大尝试并查集合并,直到连通 n + popcount(mask) 个节点。

代码

cpp
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 10005;
const int MAXK = 10;
const long long INF = (1LL << 62);

struct Edge {
    int u;
    int v;
    long long w;
};

int n, m, k;
long long city_cost[MAXK];
long long subset_cost[1 << MAXK];
vector<Edge> original_edges;
vector<Edge> mst_edges;
vector<Edge> town_edges;

int fa[MAXN + MAXK], sz[MAXN + MAXK];

bool cmp_edge(const Edge &a, const Edge &b) {
    return a.w < b.w;
}

void init_dsu(int total) {
    for (int i = 1; i <= total; i++) {
        fa[i] = i;
        sz[i] = 1;
    }
}

int find_set(int x) {
    while (fa[x] != x) {
        fa[x] = fa[fa[x]];
        x = fa[x];
    }
    return x;
}

bool unite_set(int x, int y) {
    int fx = find_set(x);
    int fy = find_set(y);
    if (fx == fy) {
        return false;
    }
    if (sz[fx] < sz[fy]) {
        swap(fx, fy);
    }
    fa[fy] = fx;
    sz[fx] += sz[fy];
    return true;
}

void build_original_mst() {
    sort(original_edges.begin(), original_edges.end(), cmp_edge);
    init_dsu(n);

    for (int i = 0; i < (int)original_edges.size(); i++) {
        Edge e = original_edges[i];
        if (unite_set(e.u, e.v)) {
            mst_edges.push_back(e);
            if ((int)mst_edges.size() == n - 1) {
                break;
            }
        }
    }
}

void build_subset_cost() {
    int total_mask = 1 << k;
    subset_cost[0] = 0;
    for (int mask = 1; mask < total_mask; mask++) {
        int lowbit = mask & -mask;
        int id = 0;
        while ((1 << id) != lowbit) {
            id++;
        }
        subset_cost[mask] = subset_cost[mask ^ lowbit] + city_cost[id];
    }
}

bool edge_allowed_by_mask(const Edge &e, int mask) {
    int town_id = e.v - n - 1;
    return (mask & (1 << town_id)) != 0;
}

long long kruskal_with_towns(int mask) {
    int selected_towns = __builtin_popcount((unsigned)mask);
    int need_edges = n + selected_towns - 1;
    int picked = 0;
    long long cost = subset_cost[mask];

    init_dsu(n + k);

    int p1 = 0;
    int p2 = 0;
    while (picked < need_edges) {
        while (p2 < (int)town_edges.size() && !edge_allowed_by_mask(town_edges[p2], mask)) {
            p2++;
        }

        bool use_original = false;
        if (p1 < (int)mst_edges.size()) {
            if (p2 == (int)town_edges.size() || mst_edges[p1].w <= town_edges[p2].w) {
                use_original = true;
            }
        }

        Edge e;
        if (use_original) {
            e = mst_edges[p1];
            p1++;
        } else {
            if (p2 == (int)town_edges.size()) {
                return INF;
            }
            e = town_edges[p2];
            p2++;
        }

        if (unite_set(e.u, e.v)) {
            cost += e.w;
            picked++;
        }
    }

    return cost;
}

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

    cin >> n >> m >> k;
    original_edges.reserve(m);
    town_edges.reserve(n * k);

    for (int i = 1; i <= m; i++) {
        Edge e;
        cin >> e.u >> e.v >> e.w;
        original_edges.push_back(e);
    }

    for (int j = 0; j < k; j++) {
        cin >> city_cost[j];
        for (int i = 1; i <= n; i++) {
            long long x;
            cin >> x;
            Edge e;
            e.u = i;
            e.v = n + j + 1;
            e.w = x;
            town_edges.push_back(e);
        }
    }

    build_original_mst();
    sort(town_edges.begin(), town_edges.end(), cmp_edge);
    build_subset_cost();

    long long ans = INF;
    int total_mask = 1 << k;
    for (int mask = 0; mask < total_mask; mask++) {
        ans = min(ans, kruskal_with_towns(mask));
    }

    cout << ans << '\n';
    return 0;
}

复杂度

原图边排序为 O(mlogm)O(m log m),乡镇边排序为 O(nklog(nk))O(nk \log(nk))

枚举 2k2^k 个乡镇集合,每个集合扫描最多 O(nk)O(nk) 条乡镇边和 O(n)O(n) 条原图 MST 边,因此总时间复杂度约为:

O(mlogm+nklog(nk)+2knkα(n+k))O(m \log m + nk \log(nk) + 2^k \cdot nk \cdot \alpha(n+k))

空间复杂度为:

O(m+nk)O(m + nk)

总结

本题不能只看到“枚举乡镇集合 + MST”,还要处理 m 很大的瓶颈。

原图 MST 的替换性质是核心:不管选了哪些乡镇,原有城市之间都不需要非 MST 边。这样每个集合下的 Kruskal 就从处理百万条原图边,变成只处理 n-1 条原图 MST 边和少量乡镇边。