P14362 [CSP-S 2025] 道路修复

枚举乡镇子集 + Kruskal:把大图先缩成原图 MST,再加入乡镇连边。

课堂版 先建模 再看代码

一、先回答问题

问题本质

选择若干乡镇作为额外节点,用最低费用让原城市连通。

容易走偏

直接对每个子集跑 m 条原边的 Kruskal,状态数乘上 m 会过大。

二、核心观察

把题目变成模型
建模固定乡镇集合后,在原城市和这些乡镇组成的图上求 MST。
优化原有道路先求 MST,只保留 n-1 条边;每个子集不再扫描 10^6 条原道路。
正确性加入额外边后,原图中真正需要参与竞争的原边可以由原图 MST 提供;非 MST 原边不会成为必要选择。

三、互动演示

--
--
--

四、自己选答案

问题 1

为什么要枚举乡镇子集?

问题 2

费用必须用什么类型?

五、手推结果

阶段边集合作用
预处理原图全部 m 条边求原城市 MST,保留 n-1 条
枚举 mask被选乡镇到城市的边决定哪些乡镇可用
KruskalMST 边 + mask 乡镇边连接原城市和被选乡镇
取最小所有 mask得到全局最优

六、C++14 参考代码

可提交代码
#include <bits/stdc++.h>
using namespace std;

struct DSU {
    vector<int> fa, sz;

    DSU(int n = 0) { init(n); }

    void init(int n) {
        fa.resize(n + 1);
        sz.assign(n + 1, 1);
        iota(fa.begin(), fa.end(), 0);
    }

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

    bool unite(int x, int y) {
        x = find(x);
        y = find(y);
        if (x == y) return false;
        if (sz[x] < sz[y]) swap(x, y);
        fa[y] = x;
        sz[x] += sz[y];
        return true;
    }
};

struct Edge {
    int u, v;
    long long w;
    int town; // -1 表示原城市道路,否则表示属于哪个乡镇

    bool operator<(const Edge &other) const {
        return w < other.w;
    }
};

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

    int n, m, k;
    cin >> n >> m >> k;

    vector<Edge> original;
    original.reserve(m);
    for (int i = 0; i < m; i++) {
        int u, v;
        long long w;
        cin >> u >> v >> w;
        original.push_back({u, v, w, -1});
    }

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

    sort(original.begin(), original.end());

    DSU dsu(n + k);
    vector<Edge> useful;
    useful.reserve(max(0, n - 1) + k * n);

    // 原图中只保留 MST 边。
    for (const auto &e : original) {
        if (dsu.unite(e.u, e.v)) useful.push_back(e);
    }

    for (int j = 0; j < k; j++) {
        int townId = n + j + 1;
        for (int i = 1; i <= n; i++) {
            useful.push_back({townId, i, a[j][i], j});
        }
    }
    sort(useful.begin(), useful.end());

    int totalMask = 1 << k;
    vector<long long> townCost(totalMask, 0);
    for (int mask = 1; mask < totalMask; mask++) {
        int bit = __builtin_ctz(mask);
        townCost[mask] = townCost[mask ^ (1 << bit)] + c[bit];
    }

    const long long INF = (1LL << 62);
    long long ans = INF;

    for (int mask = 0; mask < totalMask; mask++) {
        dsu.init(n + k);
        long long cur = townCost[mask];
        int need = n + __builtin_popcount((unsigned)mask) - 1;
        int got = 0;

        if (need == 0) {
            ans = min(ans, cur);
            continue;
        }

        for (const auto &e : useful) {
            if (e.town != -1 && ((mask >> e.town) & 1) == 0) continue;
            if (dsu.unite(e.u, e.v)) {
                cur += e.w;
                got++;
                if (got == need) break;
            }
        }

        if (got == need) ans = min(ans, cur);
    }

    cout << ans << '\n';
    return 0;
}
`town = -1` 表示原城市 MST 边。
每个 `mask` 都重新初始化并查集。
`need = n + popcount(mask) - 1` 是当前生成树需要的边数。