2025CSP-S组 T2 部分分做法

S组 T2

部分分分析很重要,如果全部分类写出,可以得72分,虽没想出最后正解,但也很高了
想得到满分,就要注意到n比m小, k<=10, 也很小。这两个就是瓶颈。

P14362 [CSP-S 2025] 道路修复 / road(官方数据)

【解析】

16 分:(k ≤ 0)

\(此时 k ≤ 0,不需要考虑乡镇对城市带来的影响。因此可以直接使用 kruskal 算法来计算出最小生成树的边权总和即可。具体地,将 m 条边按照边权从小到大排序,然后依次考虑连接每一条边后是否会形成环。若不形成环,则加入这条边;否则不加入。判断是否形成环,可以使用并查集。时间复杂度 O (m log m)。\)

另外 32 分:

\((特殊性质 A)此时 c_j = 0,且对于每个乡镇 j,都存在一个城市 i 满足 a_{j,i} = 0。这说明将任何一个乡镇都可以通过一个边权为 0 的边加入到一个已经连通的图中,即需要付出的代价是 0。而如果是中途将乡镇与未连通图中的某个连通块进行连接(前提是这个连通块中存在与乡镇边权为 0 的城市),然后再通过城市之间的边将每个城市进行连通,那么仍然能够发现使所有乡镇与城市连通需要的代价还是 0。 容易得到一个贪心的想法:既然任何一个时刻将乡镇与城市进行连通的代价都是 0,那么不妨一开始就让所有乡镇与它对应的 0 代价城市相连接。这样一来,就可以在一开始不费任何代价地产生尽可能多的边权更小的边,这些边都是乡镇与城市之间的边,用于原图中的城市相互之间进行连接。具体地,对于乡镇 j 进行如下额外的连边: 令乡镇的编号为 n + j,城市的编号为 i (1 ≤ i ≤ n); 那么在 n + j 与 i 之间连接一条边权为 a_{j,i} 的边。 随后,将这些额外的边与原本城市之间的边一同排序,进行一次 kruskal 算法即可计算出答案,注意使用 long long。设考虑到 m > nk,且 n > k,时间复杂度依然是 O (m log m)。 \)

另外 24 分:(k ≤ 5)

\( 注意到 k ≤ 5 很小,可以考虑 dfs 枚举每个乡镇是否使用。具体的做法与上述 “特殊性质 A” 的做法类似,对于枚举到的每一种使用乡镇的情况,只需要把用到的乡镇对应的边额外加入进来即可,以便产生权值更小的边用于城市之间的连通。dfs 部分的复杂度是 O (2^k),总时间复杂度是 O (2^k m log m)。 \)

100 分:

\(复杂度的瓶颈在于每次都将 m 条边重新排序了,需要考虑将 O (m log m) 的排序从 2^k 中提取出来。不难发现,m ≤ 10^6 虽然很大,但是 n ≤ 10^4 比较小,能够构成生成树的边至多有 O (n) 条。于是我们可以先对城市之间的 m 条边进行一次 kruskal,提取出其中有用的 n-1 条树边。 这样一来对于 dfs 枚举到的每一种乡镇情况,总边数就是 O (nk) 级别而非 O (m + nk),时间复杂度优化到了 O (m log m + 2^k nk log (nk))。然而,这样的复杂度可能还不足以通过本题。 考虑到每次还是会对边重新排序,为了避免这种情况,我们其实完全可以在 dfs 之前,提前将所有的 nk 条边与 n-1 条树边放在一起排序。为每条边标记上这条边来自于哪一个乡镇,这样一来,每次只需要枚举 nk + n - 1 条边,根据边的标记来判断对应的乡镇当前是否可以使用,进而判断当前的这条边是否可以选上。时间复杂度进一步降到了 O (m log m + 2^k nk)。 \)

16 分( \(k \le 0\) ):只需要对原有 \(m\) 条边做一次 Kruskal

思路简述:当 \(k\le0\) 时没有乡镇的影响,问题退化为对 \(n\) 个城市在给定 \(m\) 条边权下求最小生成树(MST)。经典 Kruskal:把所有边按权排序,逐条加入并查集(若不构成环则加入),最终取权和。

// 16_points.cpp
// 16 分解法:k <= 0 时,直接对 m 条边做 Kruskal 求 MST
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

struct DSU {
    int n;
    vector<int> p, sz;
    DSU(int n=0){ init(n); }
    void init(int _n){
        n = _n;
        p.resize(n+1);
        sz.assign(n+1,1);
        for(int i=1;i<=n;i++) p[i]=i;
    }
    int find(int x){ return p[x]==x?x:p[x]=find(p[x]); }
    bool unite(int a,int b){
        a=find(a); b=find(b);
        if(a==b) return false;
        if(sz[a]<sz[b]) swap(a,b);
        p[b]=a; sz[a]+=sz[b];
        return true;
    }
};

struct Edge {
    int u,v;
    ll w;
    Edge(){}
    Edge(int _u,int _v,ll _w):u(_u),v(_v),w(_w){}
};

int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int n,m,k;
    if(!(cin>>n>>m>>k)) return 0;
    vector<Edge> edges;
    edges.reserve(m);
    for(int i=0;i<m;i++){
        int u,v; ll w;
        cin>>u>>v>>w;
        edges.emplace_back(u,v,w);
    }
    // k <= 0 时忽略乡镇输入(若有),但题中会给 k,这里按约定 k<=0 时没有乡镇数据
    sort(edges.begin(), edges.end(), [](const Edge& a,const Edge& b){ return a.w < b.w; });
    DSU dsu(n);
    dsu.init(n);
    ll ans = 0;
    int cnt = 0;
    for(auto &e: edges){
        if(dsu.unite(e.u, e.v)){
            ans += e.w;
            ++cnt;
            if(cnt == n-1) break;
        }
    }
    cout << ans << '\n';
    return 0;
}

32 分(特殊性质 A):每个乡镇有 \(c_j=0\) 且存在某城市 \(i\) 使得 \(a_{j,i}=0\)

思路简述:既然所有乡镇的城市化成本 \(c_j=0\) ,且每个乡镇与至少一个城市的连接代价为 0,那么没有必要枚举选择与否——把所有乡镇视作“免费可用”节点,把乡镇与所有城市之间的边(权为 \(a_{j,i}\) )全部加入图,然后对整个图做一次 Kruskal。并查集大小为 \(n+k\) ,最后只需关心把原有 \(n\) 个城市连通,乡镇作为辅助节点自动参与。

// 32_points_specialA.cpp
// 32 分解法(特殊性质 A):c_j = 0 且每个乡镇存在一个 a_{j,i} = 0
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

struct DSU {
    int n;
    vector<int> p, sz;
    DSU(int n=0){ init(n); }
    void init(int _n){
        n = _n;
        p.resize(n+1);
        sz.assign(n+1,1);
        for(int i=1;i<=n;i++) p[i]=i;
    }
    int find(int x){ return p[x]==x?x:p[x]=find(p[x]); }
    bool unite(int a,int b){
        a=find(a); b=find(b);
        if(a==b) return false;
        if(sz[a]<sz[b]) swap(a,b);
        p[b]=a; sz[a]+=sz[b];
        return true;
    }
};

struct Edge {
    int u,v;
    ll w;
    Edge(){}
    Edge(int _u,int _v,ll _w):u(_u),v(_v),w(_w){}
};

int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int n,m,k;
    if(!(cin>>n>>m>>k)) return 0;
    vector<Edge> edges;
    edges.reserve(m + (ll)k * n);
    for(int i=0;i<m;i++){
        int u,v; ll w; cin>>u>>v>>w;
        edges.emplace_back(u,v,w);
    }
    vector<ll> c(k);
    vector<vector<ll>> a(k, vector<ll>(n+1));
    for(int j=0;j<k;j++){
        cin>>c[j];
        for(int i=1;i<=n;i++) cin>>a[j][i];
    }
    // 因为 c_j = 0,且至少存在 a[j][i]=0(题设保证),我们把所有乡镇节点以及它们与城市之间的边都加入
    // 乡镇编号使用 n+1 .. n+k
    for(int j=0;j<k;j++){
        int town = n + 1 + j;
        for(int i=1;i<=n;i++){
            edges.emplace_back(town, i, a[j][i]);
        }
    }
    sort(edges.begin(), edges.end(), [](const Edge& A, const Edge& B){ return A.w < B.w; });
    DSU dsu(n+k);
    dsu.init(n+k);
    ll ans = 0;
    for(auto &e: edges){
        if(dsu.unite(e.u, e.v)){
            ans += e.w;
        }
    }
    cout << ans << '\n';
    return 0;
}

24 分( \(k \le 5\) ):对乡镇选择做枚举(DFS / 位掩码),每种情况做 Kruskal(朴素版)

思路简述:因为 \(k\le5\) ,可以枚举每个乡镇是否“被选用”(共 \(2^k \le 32\) 种)。对于每个掩码,构造边集合:原有 \(m\) 条边 + 对被选乡镇加入其与所有城市的 \(n\) 条边(乡镇视作节点 \(n+j\) ),然后对该集合排序并 Kruskal。最后答案为所有掩码的最小值。注意用 long long,每种掩码独立做排序(实现简单但在极端 m 较大时会慢——但题目这档给分中允许此做法)。

提示:该实现直接重新排序 \(m + s\cdot n\) 条边每次,适用于 \(k\le5\) 的情况。

// 24_points_k_le_5.cpp
// 24 分解法:k <= 5,枚举乡镇是否使用(位掩码),每个掩码构造边集并 Kruskal
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

struct DSU {
    int n;
    vector<int> p, sz;
    DSU(int n=0){ init(n); }
    void init(int _n){
        n = _n;
        p.resize(n+1);
        sz.assign(n+1,1);
        for(int i=1;i<=n;i++) p[i]=i;
    }
    int find(int x){ return p[x]==x?x:p[x]=find(p[x]); }
    bool unite(int a,int b){
        a=find(a); b=find(b);
        if(a==b) return false;
        if(sz[a]<sz[b]) swap(a,b);
        p[b]=a; sz[a]+=sz[b];
        return true;
    }
};

struct Edge {
    int u,v;
    ll w;
    Edge(){}
    Edge(int _u,int _v,ll _w):u(_u),v(_v),w(_w){}
};

int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int n,m,k;
    if(!(cin>>n>>m>>k)) return 0;
    vector<Edge> baseEdges;
    baseEdges.reserve(m);
    for(int i=0;i<m;i++){
        int u,v; ll w; cin>>u>>v>>w;
        baseEdges.emplace_back(u,v,w);
    }
    vector<ll> c(k);
    vector<vector<ll>> a(k, vector<ll>(n+1));
    for(int j=0;j<k;j++){
        cin>>c[j];
        for(int i=1;i<=n;i++) cin>>a[j][i];
    }

    ll answer = (1LL<<62);
    int maxMask = 1<<k;
    // 枚举所有掩码
    for(int mask=0; mask<maxMask; ++mask){
        // 1. 构造当前掩码下的边集合:先把原始 m 条边放入
        vector<Edge> edges = baseEdges;
        // 2. 对于选中的乡镇 j,加入其与所有城市的 n 条边(乡镇编号 n+j+1)
        ll sumC = 0;
        for(int j=0;j<k;j++){
            if(mask & (1<<j)){
                sumC += c[j];
                int town = n + 1 + j;
                for(int i=1;i<=n;i++){
                    edges.emplace_back(town, i, a[j][i]);
                }
            }
        }
        // 3. 排序并 Kruskal(并查集大小 n + number of selected towns)
        sort(edges.begin(), edges.end(), [](const Edge& A, const Edge& B){ return A.w < B.w; });
        int nodes = n;
        for(int j=0;j<k;j++) if(mask & (1<<j)) ++nodes; // nodes = n + popcount(mask)
        // For simplicity we set DSU size to n + k (safe)
        DSU dsu(n + k);
        dsu.init(n + k);
        ll cost = 0;
        int used = 0;
        for(auto &e: edges){
            if(dsu.unite(e.u, e.v)){
                cost += e.w;
                ++used;
                // We can stop early if all original cities are connected:
                // However to keep code simple we let loop continue; it's acceptable given constraints in this score tier.
            }
        }
        // Final total cost: sumC + cost
        answer = min(answer, cost + sumC);
    }
    cout << answer << '\n';
    return 0;
}

100 分(完整高效解):一次对原 \(m\) 条边 Kruskal 提取 \(n-1\) 条有用边,预排序 \(n-1 + n\cdot k\) 条边,枚举掩码时线性扫描(不重复排序)

思路简述(关键点):

  1. 先对原始 \(m\) 条边做一次 Kruskal,得到一组“候选城市间边”(最多 \(n-1\) 条),因为在任意添加乡镇的情况下,真正可能出现在最终解的城市—城市边只来自于原有边的 MST(这是一种剪枝/启发:保留原图 Kruskal 得到的树边作为候选)。

    备注:这里保留 MST 的 \(n-1\) 条边能够明显降低之后每个掩码下需要考虑的城市—城市边数。

  2. 对每个乡镇 \(j\) ,把它与每个城市 \(i\) 的边(权 \(a_{j,i}\) )都视为一条边,标记这条边来自乡镇 \(j\) (town id)。所有这些 \(n\cdot k\) 条边与上一步得到的 \(n-1\) 条树边合并到一个数组里,并只做一次排序(按权)。每条边要额外保存一个 town_id 字段(0 表示原来城市—城市树边,  \(\,1..k\) 表示来自哪个乡镇)。
  3. 枚举所有乡镇子集(掩码)。对于给定掩码,从排序好的数组线性扫描:遇到 town_id==0 的树边就尝试 unite;遇到 town_id==j>0 的边,只有当掩码中包含该乡镇(即我们“投资”了该乡镇)时才尝试 unite;其它边跳过。这样每个掩码只需 \(O(nk)\) 扫描并查操作(无需重新排序 \(m\) 条边)。时间复杂度约为 \(O(m\log m + 2^k \cdot nk)\) ,能通过题目最大数据。

下面是实现(注意并查集大小为 \(n+k\) ,乡镇编号为 \(n+1\ldots n+k\) ):

// 100_points.cpp
// 100 分解法:先对 m 边 Kruskal 取出 n-1 条树边,然后把 n-1 + n*k 条边排序,枚举掩码时线性扫描
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll INFLL = (1LL<<62);

struct DSU {
    int n;
    vector<int> p, sz;
    DSU(int n=0){ init(n); }
    void init(int _n){
        n = _n;
        p.resize(n+1);
        sz.assign(n+1,1);
        for(int i=1;i<=n;i++) p[i]=i;
    }
    int find(int x){ return p[x]==x?x:p[x]=find(p[x]); }
    bool unite(int a,int b){
        a=find(a); b=find(b);
        if(a==b) return false;
        if(sz[a]<sz[b]) swap(a,b);
        p[b]=a; sz[a]+=sz[b];
        return true;
    }
};

struct Edge {
    int u, v;
    ll w;
    int town_id; // 0 表示原来城市-城市的 MST 边;1..k 表示来自第 j 个乡镇 (与城市的边)
    Edge(){}
    Edge(int _u,int _v,ll _w,int _t):u(_u),v(_v),w(_w),town_id(_t){}
};

int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int n, m, k;
    if(!(cin>>n>>m>>k)) return 0;
    vector<Edge> allOriginal;
    allOriginal.reserve(m);
    // 读入原有 m 条边(城市编号 1..n)
    for(int i=0;i<m;i++){
        int u,v; ll w; cin>>u>>v>>w;
        allOriginal.emplace_back(u, v, w, 0); // 暂时 town_id=0(只是原始边)
    }

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

    // 1) 先对原始 m 边做一次 Kruskal,抽取出构成 MST 的至多 n-1 条边(作为候选城市-城市边)
    vector<Edge> tmp = allOriginal;
    sort(tmp.begin(), tmp.end(), [](const Edge& A, const Edge& B){ return A.w < B.w; });
    DSU dsu0(n);
    dsu0.init(n);
    vector<Edge> cityTreeEdges; cityTreeEdges.reserve(n-1);
    for(auto &e: tmp){
        if(dsu0.unite(e.u, e.v)){
            cityTreeEdges.emplace_back(e.u, e.v, e.w, 0); // town_id 仍为 0 标记为城市边
            if((int)cityTreeEdges.size() == n-1) break;
        }
    }
    // 2) 构造 n*k 条乡镇-城市边(编号 town = n + j)
    //    每条标记其 town_id = j+1 (1..k)
    vector<Edge> combined;
    combined.reserve((int)cityTreeEdges.size() + n * max(0,k));
    // 先插入那些城市间的树边(town_id = 0)
    for(auto &e: cityTreeEdges) combined.push_back(e);
    // 再插入所有乡镇-城市边
    for(int j=0;j<k;j++){
        int town = n + 1 + j;
        for(int i=1;i<=n;i++){
            combined.emplace_back(town, i, a[j][i], j+1);
        }
    }
    // 3) 对 combined 统一排序(按权)
    sort(combined.begin(), combined.end(), [](const Edge& A, const Edge& B){
        if(A.w != B.w) return A.w < B.w;
        // 为稳定性,优先原始城市-城市边(town_id == 0),但不必严格要求
        return A.town_id < B.town_id;
    });

    // 4) 枚举掩码(2^k),对每个掩码线性扫描 combined,使用 DSU(n+k)
    ll answer = INFLL;
    int maxMask = 1<<k;
    for(int mask=0; mask<maxMask; ++mask){
        DSU dsu(n + k);
        dsu.init(n + k);
        ll baseCost = 0; // 城市/乡镇边的总费用
        ll sumC = 0; // 被选中乡镇的一次性费用
        for(int j=0;j<k;j++) if(mask & (1<<j)) sumC += c[j];

        int used = 0; // 不严谨计数,下面我们只在合并时累加权重
        // 我们要保证原始城市 1..n 最终连通(通过乡镇参与或直接城市边)
        for(const Edge &e: combined){
            if(e.town_id == 0){
                // 城市-城市树边,总是可用
                if(dsu.unite(e.u, e.v)){
                    baseCost += e.w;
                }
            } else {
                int j = e.town_id - 1;
                if( (mask & (1<<j)) == 0 ) continue; // 未选择该乡镇,跳过
                // 否则乡镇可用,把乡镇节点编号 (n+1+j) 与城市 i 连接
                if(dsu.unite(e.u, e.v)){
                    baseCost += e.w;
                }
            }
        }

        ll total = baseCost + sumC;
        if(total < answer) answer = total;
    }
    cout << answer << '\n';
    return 0;
}

各种做法时间复杂度

🌟【16分做法】( \(k = 0\)

思路:

只有城市和道路,不存在乡镇。
→ 直接在 \(n\) 个城市、 \(m\) 条边上跑 Kruskal 最小生成树算法

步骤复杂度分析:

  1. 排序所有边
    \(O(m \log m)\)
  2. 并查集合并查找(近似 \(O(1)\) 摊还):
    \(m\) 次边判断,复杂度约 \(O(m \alpha(n)) \approx O(m)\)
  3. 合并总复杂度
    \(O(m \log m + m) \Rightarrow O(m \log m)\)

最终复杂度\(O(m \log m)\)
适用数据\(k = 0\) ,或忽略乡镇的情况。


🌟【32分做法】(特殊性质 A)

条件:

  • \(c_j = 0\)
  • 存在某城市 \(i\) ,使 \(a_{j,i} = 0\)

思路:

每个乡镇都能零代价连上某个城市,因此相当于所有乡镇都已免费接入。
→ 直接将所有乡镇视为新的节点,与城市相连的边加入图后,用 Kruskal 求最小生成树。

边数:

  • 原边: \(m\)
  • 新增边: \(n \times k\)

步骤复杂度:

  1. 排序所有边(包括城市间 + 城市到乡镇):

    \[O((m + nk) \log (m + nk)) \]

  2. 并查集操作: \(O(m + nk)\)
  3. 总体复杂度

    \[O((m + nk) \log (m + nk)) \]

✅ 由于 \(k \le 10\)\(nk \ll m\) ,所以可简化为:

\[O(m \log m) \]


🌟【24分做法】( \(k \le 5\) ,枚举子集)

思路:

\(k\) 很小,可以 枚举每个乡镇是否使用\(2^k\) 种情况)。
对于每种情况:

  • 加入该乡镇的所有边;
  • 然后对“城市 + 当前选中的乡镇”做一次 Kruskal。

步骤复杂度:

  1. 枚举所有乡镇组合: \(2^k\)
  2. 每次 Kruskal:

    \[O((m + nk) \log (m + nk)) \]

  3. 总复杂度:

    \[O(2^k (m + nk) \log (m + nk)) \]

  4. 因为 \(k \le 5\) ,可近似:

    \[O(2^k m \log m) = O(32 m \log m) \]

总结:

\[O(2^k m \log m) \]


🌟【100分做法】(终极优化)

核心优化思想:

前面做法瓶颈在于:

每次枚举都要重新排序 \(m\) 条边!

我们注意到:

  • Kruskal 选出的 有效边 只有 \(n - 1\) 条;
  • 枚举的边中,大多数在所有情况中都没用。

所以优化思路是:

  1. 先用 Kruskal 在城市之间跑一遍,找到 \(n - 1\) 条生成树边;
  2. 只保留这些边 + 所有乡镇边( \(n \times k\) 条);
  3. 一次性排序\(O(nk + n)\) 条边;
  4. DFS 枚举每种乡镇选法,在 Kruskal 时只考虑标记允许的边。

步骤复杂度:

  1. 第一次 Kruskal(提取城市生成树)
    \(O(m \log m)\)
  2. 一次性排序所有 \(nk + n - 1\) 条边
    \(O((nk + n) \log (nk + n)) \approx O(nk \log (nk))\)
  3. 枚举 \(2^k\) 种乡镇选法,每次 Kruskal 边数 O(nk)

    \[O(2^k nk) \]

  4. 总复杂度

    \[O(m \log m + 2^k nk) \]

✅ 对于题目范围 \(n = 10^4, m = 10^6, k = 10\) ,这能完美通过。


✅ 最终汇总表

分值 思路 时间复杂度 关键优化
16分 无乡镇,纯 Kruskal \(O(m \log m)\) 直接求城市最小生成树
32分 特殊性质 A,所有乡镇 0 成本接入 \(O((m + nk)\log(m + nk)) \approx O(m \log m)\) 把乡镇直接加进图中
24分 枚举乡镇使用情况 \(O(2^k m \log m)\) DFS 枚举每个乡镇选法
100分 先提取城市树边 + 预排序 \(O(m \log m + 2^k nk)\) 只枚举有限边集,提前排序

整合前面那个城市联通最小费用的16分 ,32分,24分做法, 不用100分做法,形成一个部分分合并的最优得分解答

整体策略(自适应选择三种方案)

读取输入后判断三种情况的优先级(按最简单/最安全的顺序):

  1. ** \(k = 0\) **(16分情况)
    直接对原有 \(m\) 条边做一次 Kruskal 求最小生成树(MST)。复杂度 \(O(m\log m)\)
  2. 特殊性质 A(32分):对于每个乡镇 \(j\) ,都有 \(c_j = 0\) 且存在某城市 \(i\) 使得 \(a_{j,i} = 0\)
    既然乡镇免费且至少能以 0 费用接入一个城市,就把所有乡镇当作额外节点,把乡镇到所有城市的边(权为 \(a_{j,i}\) )全部加入图,然后对包含 \(n+k\) 个节点的一次 Kruskal。复杂度近似 \(O((m + nk)\log(m+nk))\) ,通常可视为 \(O(m\log m)\)
  3. ** \(k \le 5\) **(24分)
    枚举每个乡镇是否被选择(共 \(2^k\) 种情况)。对于每个子集:加入对应乡镇与城市的边(乡镇编号为 \(n+1..n+k\) ),再与原有边一起做 Kruskal,计算总费用(一次性费用 \(\sum c_j\) 加上边修建/修复费用)。复杂度 \(O(2^k (m + nk)\log(m+nk))\) ,因为 \(k\le5\) 可行。

程序按上述顺序判断并执行:若满足 \(k=0\) 执行 16 分分支;否则若满足特殊 A 执行 32 分分支;否则若 \(k\le5\) 则执行枚举分支;若以上都不满足(例如 \(k>5\) 且不满足 A),程序仍会尝试做枚举分支(可能超时——这是你要求“只用 16/24/32 分做法,不用 100 分做法”的固有限制)。


代码(C++,OI 风格,详注释)

// road_partial.cpp
// 整合 16 分(k=0)、32 分(特殊性质 A)、24 分(k<=5 枚举)三种做法的程序(不含 100 分优化)
// 编译:g++ -O2 -std=c++17 road_partial.cpp -o road_partial
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll INFLL = (1LL<<62);

// 并查集(OI 风格)
struct DSU {
    int n;
    vector<int> fa, sz;
    DSU(int n_=0){ init(n_); }
    void init(int n_){
        n = n_;
        fa.resize(n+1);
        sz.assign(n+1, 1);
        for(int i=1;i<=n;i++){ fa[i]=i; }
    }
    int find(int x){ return fa[x]==x ? x : fa[x]=find(fa[x]); }
    bool unite(int a,int b){
        a = find(a); b = find(b);
        if(a==b) return false;
        if(sz[a] < sz[b]) swap(a,b);
        fa[b] = a;
        sz[a] += sz[b];
        return true;
    }
};

// 基本边结构:连接 u-v,权重 w
struct Edge {
    int u, v;
    ll w;
    Edge(){}
    Edge(int _u,int _v,ll _w):u(_u),v(_v),w(_w){}
};

int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int n, m, k;
    if(!(cin >> n >> m >> k)) return 0;

    // 读取原有 m 条边
    vector<Edge> baseEdges;
    baseEdges.reserve(m);
    for(int i=0;i<m;i++){
        int u,v; ll w;
        cin >> u >> v >> w;
        baseEdges.emplace_back(u,v,w);
    }

    // 读取 k 个乡镇的数据(如果 k==0 则不会读到下面)
    vector<ll> c(k);
    vector<vector<ll>> a(k, vector<ll>(n+1));
    for(int j=0;j<k;j++){
        cin >> c[j];
        for(int i=1;i<=n;i++){
            cin >> a[j][i];
        }
    }

    // ---------- 判定特殊性质 A(32 分所需) ----------
    bool specialA = true;
    for(int j=0;j<k;j++){
        if(c[j] != 0){ specialA = false; break; }
        bool hasZero = false;
        for(int i=1;i<=n;i++){
            if(a[j][i] == 0){ hasZero = true; break; }
        }
        if(!hasZero){ specialA = false; break; }
    }

    // ---------- 分支 1:k == 0(16 分) ----------
    if(k == 0){
        // 只需对原有 m 条边做一次 Kruskal,连通 n 个城市
        sort(baseEdges.begin(), baseEdges.end(), [](const Edge& A, const Edge& B){
            return A.w < B.w;
        });
        DSU dsu(n);
        dsu.init(n);
        ll ans = 0;
        int used = 0;
        for(const auto &e: baseEdges){
            if(dsu.unite(e.u, e.v)){
                ans += e.w;
                if(++used == n-1) break;
            }
        }
        cout << ans << "\n";
        return 0;
    }

    // ---------- 分支 2:特殊性质 A(32 分) ----------
    if(specialA){
        // 将所有乡镇视作额外节点(编号 n+1 .. n+k),把所有乡镇-城市的边加入,再与 baseEdges 一起 Kruskal
        // 边数为 m + n*k;并查集节点数为 n + k
        vector<Edge> edges = baseEdges;
        edges.reserve(m + (ll) n * k);
        for(int j=0;j<k;j++){
            int town = n + 1 + j;
            for(int i=1;i<=n;i++){
                edges.emplace_back(town, i, a[j][i]);
            }
        }
        sort(edges.begin(), edges.end(), [](const Edge& A, const Edge& B){
            return A.w < B.w;
        });
        DSU dsu(n + k);
        dsu.init(n + k);
        ll total = 0;
        for(const auto &e: edges){
            if(dsu.unite(e.u, e.v)){
                total += e.w;
            }
        }
        // 题目目标是保证原有 n 个城市两两连通;在特殊 A 下把所有乡镇免费加入并做 MST 可以得到最优或等价解
        cout << total << "\n";
        return 0;
    }

    // ---------- 分支 3:k <= 5(24 分) ----------
    if(k <= 5){
        // 枚举所有乡镇子集(2^k <= 32),每个子集把被选乡镇加入以及其与城市的边加入,再与 baseEdges 一起 Kruskal
        ll best = INFLL;
        int maxMask = 1<<k;

        // 为减少常量,先把 baseEdges 排序一次;在每个掩码中我们将 baseEdges 复制到局部数组再加入乡镇边并排序
        // 注意:这仍然会重新排序大数组,但在 k<=5 情况常被允许(部分分)
        sort(baseEdges.begin(), baseEdges.end(), [](const Edge& A, const Edge& B){
            return A.w < B.w;
        });

        for(int mask = 0; mask < maxMask; ++mask){
            // 计算一次性费用 sumC
            ll sumC = 0;
            int addedTowns = 0;
            for(int j=0;j<k;j++){
                if(mask & (1<<j)){
                    sumC += c[j];
                    ++addedTowns;
                }
            }
            // 构造边集:把 baseEdges 复制,然后加入被选乡镇的 n 条边
            vector<Edge> edges = baseEdges; // 已排序,但后续插入会破坏顺序 -> 下面要重新排序
            edges.reserve(edges.size() + (size_t)addedTowns * n);
            for(int j=0;j<k;j++){
                if(mask & (1<<j)){
                    int town = n + 1 + j;
                    for(int i=1;i<=n;i++){
                        edges.emplace_back(town, i, a[j][i]);
                    }
                }
            }
            // 重新排序(简洁直观):时间瓶颈在 m 较大时会慢,但属于 24 分的允许实现
            sort(edges.begin(), edges.end(), [](const Edge& A, const Edge& B){
                return A.w < B.w;
            });

            // Kruskal:并查集大小使用 n + k(安全)
            DSU dsu(n + k);
            dsu.init(n + k);
            ll cost = 0;
            for(const auto &e: edges){
                if(dsu.unite(e.u, e.v)){
                    cost += e.w;
                }
            }
            // 最终总费用 = 修/建道路费用 + 被选乡镇的一次性费用
            best = min(best, cost + sumC);
        }
        cout << best << "\n";
        return 0;
    }

    // ---------- 其它情况(k > 5 且不满足特殊 A) ----------
    // 由于本次任务要求不使用 100 分的优化,这里我们仍然尝试用枚举(可能超时),并给出提示性实现。
    // 实际比赛中对这种情况应该用 100 分的优化版本(已在之前讨论)。
    {
        // 尝试枚举(若 k 较大会爆炸),但实现与上面 k<=5 相同(注意可能超时)
        ll best = INFLL;
        int maxMask = 1<<k;
        if(k >= 25){
            // 防止位移溢出或枚举不可行,直接退出或返回一个安全上界(但题设要求不这样)
            // 这里我们为了完整性,直接使用一种保守做法:只做不选任何乡镇(等同 k=0)
            // 这保证程序不会爆炸,但得分可能较低。
            sort(baseEdges.begin(), baseEdges.end(), [](const Edge& A, const Edge& B){ return A.w < B.w; });
            DSU dsu(n);
            dsu.init(n);
            ll ans = 0;
            int used = 0;
            for(const auto &e: baseEdges){
                if(dsu.unite(e.u, e.v)){
                    ans += e.w;
                    if(++used == n-1) break;
                }
            }
            cout << ans << "\n";
            return 0;
        }
        // 仍尝试完整枚举(可能会超时)
        sort(baseEdges.begin(), baseEdges.end(), [](const Edge& A, const Edge& B){
            return A.w < B.w;
        });
        for(int mask = 0; mask < maxMask; ++mask){
            ll sumC = 0;
            int addedTowns = 0;
            for(int j=0;j<k;j++){
                if(mask & (1<<j)){
                    sumC += c[j];
                    ++addedTowns;
                }
            }
            vector<Edge> edges = baseEdges;
            edges.reserve(edges.size() + (size_t)addedTowns * n);
            for(int j=0;j<k;j++){
                if(mask & (1<<j)){
                    int town = n + 1 + j;
                    for(int i=1;i<=n;i++){
                        edges.emplace_back(town, i, a[j][i]);
                    }
                }
            }
            sort(edges.begin(), edges.end(), [](const Edge& A, const Edge& B){
                return A.w < B.w;
            });
            DSU dsu(n + k);
            dsu.init(n + k);
            ll cost = 0;
            for(const auto &e: edges){
                if(dsu.unite(e.u, e.v)) cost += e.w;
            }
            best = min(best, cost + sumC);
        }
        cout << best << "\n";
        return 0;
    }
}

各分支的时间复杂度总结(清晰列出)

  • **16 分( \(k=0\) **):排序 \(m\) 条边: \(O(m\log m)\) 。并查集操作摊还 \(O(m)\) 。总体 \(O(m\log m)\)
  • 32 分(特殊性质 A):共 \(m + nk\) 条边,排序复杂度 \(O((m+nk)\log(m+nk))\) 。通常 \(k\le10\) ,所以可近似 \(O(m\log m)\)
  • 24 分( \(k\le5\) 枚举):枚举 \(2^k\) 个子集(≤32),每个子集做一次 Kruskal:排序与并查复杂度约 \(O((m+nk)\log(m+nk))\) 。总复杂度 \(O(2^k (m+nk)\log(m+nk))\) ,常数较大但在部分分上传统可接受。
  • 其它情况( \(k>5\) 且不满足 A):本程序尝试枚举所有子集(会爆炸),在比赛中应改用 100 分的优化(已在 earlier 讨论中给出)。为防止位移溢出,本程序若 \(k\) 极大会退化为只做 \(k=0\) 的 MST 作为保守输出(可以保证不会崩溃,但得分受限)。

posted @ 2025-11-12 10:24  katago  阅读(6)  评论(0)    收藏  举报