2025信奥赛C++提高组csp-s复赛真题及题解:道路修复

2025信奥赛C++提高组csp-s复赛真题及题解:道路修复

题目描述

C 国的交通系统由 n n n 座城市与 m m m 条连接两座城市的双向道路构成,第 i i i ( 1 ≤ i ≤ m 1 \leq i \leq m 1≤i≤m) 条道路连接城市 u i u_i ui 和 v i v_i vi。任意两座城市都能通过若干条道路相互到达。

然而,近期由于一场大地震,所有 m m m 条道路都被破坏了,修复第 i i i ( 1 ≤ i ≤ m 1 \leq i \leq m 1≤i≤m) 条道路的费用为 w i w_i wi。与此同时,C 国还有 k k k 个准备进行城市化改造的乡镇。对于第 j j j ( 1 ≤ j ≤ k 1 \leq j \leq k 1≤j≤k) 个乡镇,C 国对其进行城市化改造的费用为 c j c_j cj。在城市化改造完第 j j j ( 1 ≤ j ≤ k 1 \leq j \leq k 1≤j≤k) 个乡镇后,可以在这个乡镇与原来的 n n n 座城市间建造若干条道路,其中在它与第 i i i ( 1 ≤ i ≤ n 1 \leq i \leq n 1≤i≤n) 座城市间建造一条道路的费用为 a j , i a_{j,i} aj,i。C 国可以在这 k k k 个乡镇中选择任意多个进行城市化改造,也可以不选择任何乡镇进行城市化改造。

为尽快恢复城市间的交通,C 国希望以最低的费用将原有 的 n n n 座城市两两连通,也即任意两座原有的城市都能通过若干条修复或新建造的道路相互到达。你需要帮助他们求出,将原有的 n n n 座城市两两连通的最小费用。

输入格式

输入的第一行包含三个非负整数 n , m , k n, m, k n,m,k,分别表示原有的城市数量、道路数量和准备进行城市化改造的乡镇数量。

输入的第 i + 1 i+1 i+1 ( 1 ≤ i ≤ m 1 \leq i \leq m 1≤i≤m) 行包含三个非负整数 u i , v i , w i u_i, v_i, w_i ui,vi,wi,表示第 i i i 条道路连接的两座城市与修复该道路的费用。

输入的第 j + m + 1 j+m+1 j+m+1 ( 1 ≤ j ≤ k 1 \leq j \leq k 1≤j≤k) 行包含 n + 1 n+1 n+1 个非负整数 c j , a j , 1 , a j , 2 , ... , a j , n c_j, a_{j,1}, a_{j,2}, \ldots, a_{j,n} cj,aj,1,aj,2,...,aj,n,分别表示将第 j j j 个乡镇进行城市化改造的费用与在该乡镇与原有的城市间建造道路的费用。

输出格式

输出一行一个非负整数,表示将原有的 n n n 座城市两两连通的最小费用。

输入输出样例 1
输入 1
复制代码
4 4 2
1 4 6
2 3 7
4 2 5
4 3 4
1 1 8 2 4
100 1 3 2 4
输出 1
复制代码
13
说明/提示
【样例 1 解释】

C 国可以选择修复第 3 3 3 条和第 4 4 4 条道路,然后将第 1 1 1 个乡镇进行城市化改造,并建造它与第 1,3 座城市间的道路,总费用为 5 + 4 + 1 + 1 + 2 = 13。可以证明,不存在比 13 更小的费用能使原有的 4 座城市两两连通。

【数据范围】

对于所有测试数据,保证:

  • 1 ≤ n ≤ 10 4 1 \leq n \leq 10^4 1≤n≤104, 1 ≤ m ≤ 10 6 1 \leq m \leq 10^6 1≤m≤106, 0 ≤ k ≤ 10 0 \leq k \leq 10 0≤k≤10;
  • 对于所有 1 ≤ i ≤ m 1 \leq i \leq m 1≤i≤m,均有 1 ≤ u i , v i ≤ n 1 \leq u_i, v_i \leq n 1≤ui,vi≤n, u i ≠ v i u_i \neq v_i ui=vi 且 0 ≤ w i ≤ 10 9 0 \leq w_i \leq 10^9 0≤wi≤109;
  • 对于所有 1 ≤ j ≤ k 1 \leq j \leq k 1≤j≤k,均有 0 ≤ c j ≤ 10 9 0 \leq c_j \leq 10^9 0≤cj≤109;
  • 对于所有 1 ≤ j ≤ k 1 \leq j \leq k 1≤j≤k, 1 ≤ i ≤ n 1 \leq i \leq n 1≤i≤n,均有 0 ≤ a j , i ≤ 10 9 0 \leq a_{j,i} \leq 10^9 0≤aj,i≤109;
  • 任意两座原有的城市都能通过若干条原有的道路相互到达。
测试点编号 n ≤ n \leq n≤ m ≤ m \leq m≤ k ≤ k \leq k≤ 特殊性质
1 ∼ 4 1 \sim 4 1∼4 10 4 10^4 104 10 6 10^6 106 0 0 0
5 , 6 5, 6 5,6 10 3 10^3 103 10 5 10^5 105 5 5 5 A
7 , 8 7, 8 7,8 ^ ^ ^
9 , 10 9, 10 9,10 ^ 10 6 10^6 106 ^ A
11 , 12 11, 12 11,12 ^ ^ ^
13 , 14 13, 14 13,14 ^ ^ 10 10 10 A
15 , 16 15, 16 15,16 ^ ^ ^
17 , 18 17, 18 17,18 10 4 10^4 104 ^ 5 5 5 A
19 , 20 19, 20 19,20 ^ ^ ^
21 ∼ 25 21 \sim 25 21∼25 ^ ^ 10 10 10 ^

特殊性质 A:对于所有 1 ≤ j ≤ k 1 \leq j \leq k 1≤j≤k,均有 c j = 0 c_j = 0 cj=0 且均存在 1 ≤ i ≤ n 1 \leq i \leq n 1≤i≤n 满足 a j , i = 0 a_{j,i} = 0 aj,i=0。

思路分析

核心问题 :需要让n个城市连通,可以选择激活一些乡镇作为中转站,每个激活的乡镇需要支付城市化费用 c j c_j cj,然后可以建造从该乡镇到各城市的道路,费用为 a j , i a_{j,i} aj,i。

算法框架

  1. 预处理原图最小生成树:由于只需连接n个城市,我们可以先求出原图的最小生成树,只保留这n-1条边作为候选边。
  2. 枚举乡镇选择方案:因为k≤10,可以枚举所有2^k种激活方案。
  3. 对每个方案高效计算最小生成树
    • 构建包含n个城市和选中乡镇的图
    • 边集包括:原图MST的n-1条边 + 每个选中乡镇到所有城市的边
    • 使用多路归并Kruskal算法,避免完全排序所有边

优化技巧

  • 原图的MST边已经有序
  • 每个乡镇到城市的边可以预先排序
  • 在Kruskal算法中,使用优先队列维护当前最小边,实现多路归并

代码实现

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
const int MAXN = 10010;
const int MAXK = 10;
const ll INF = 1e18;

int n, m, k;
int fa[MAXN + MAXK];  // 并查集

struct Edge {
    int u, v, w;
    bool operator<(const Edge& e) const {
        return w < e.w;
    }
};

vector<Edge> mst_edges;  // 原图MST的边
vector<int> c(MAXK);     // 乡镇城市化费用
vector<vector<pair<int, int>>> town_edges(MAXK);  // 每个乡镇到城市的边(权值, 城市)

// 并查集函数
int find(int x) {
    return fa[x] == x ? x : fa[x] = find(fa[x]);
}

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

// 计算给定掩码下的最小生成树费用
ll solve_mask(int mask) {
    // 统计选中的乡镇
    vector<int> towns;
    ll town_cost = 0;
    for (int j = 0; j < k; j++) {
        if (mask >> j & 1) {
            towns.push_back(j);
            town_cost += c[j];
        }
    }
    
    int town_cnt = towns.size();
    int total_nodes = n + town_cnt;
    
    // 初始化并查集:城市编号1~n,乡镇编号n+1~n+town_cnt
    for (int i = 1; i <= total_nodes; i++) fa[i] = i;
    
    // 使用多路归并Kruskal算法
    // 我们需要合并:原图MST边(已排序) + 每个选中乡镇的边(已排序)
    
    // 堆中元素:(边权, 类型, 索引)
    // 类型0: 原图MST边,索引表示在mst_edges中的下标
    // 类型1: 乡镇边,索引表示乡镇在towns中的下标
    priority_queue<pair<int, pair<int, int>>, 
                   vector<pair<int, pair<int, int>>>,
                   greater<pair<int, pair<int, int>>>> pq;
    
    // 每个选中乡镇的当前边指针
    vector<int> ptr(town_cnt, 0);
    
    // 加入原图MST的第一条边
    if (!mst_edges.empty()) {
        pq.push({mst_edges[0].w, {0, 0}});  // 类型0,索引0
    }
    
    // 加入每个选中乡镇的第一条边
    for (int i = 0; i < town_cnt; i++) {
        int j = towns[i];
        if (!town_edges[j].empty()) {
            int w = town_edges[j][0].first;
            pq.push({w, {1, i}});  // 类型1,索引i(表示第i个选中的乡镇)
        }
    }
    
    // 原图MST边指针
    int old_ptr = 0;
    // 每个乡镇的边指针已在ptr中
    
    ll mst_cost = 0;
    int edges_used = 0;
    
    while (edges_used < total_nodes - 1 && !pq.empty()) {
        auto top = pq.top();
        pq.pop();
        
        int w = top.first;
        int type = top.second.first;
        int idx = top.second.second;
        
        if (type == 0) {  // 原图MST边
            if (old_ptr >= mst_edges.size()) continue;
            
            const Edge& e = mst_edges[old_ptr];
            if (unite(e.u, e.v)) {
                mst_cost += w;
                edges_used++;
            }
            
            // 加入下一条原图边
            old_ptr++;
            if (old_ptr < mst_edges.size()) {
                pq.push({mst_edges[old_ptr].w, {0, old_ptr}});
            }
        }
        else {  // 乡镇边
            int i = idx;  // 第i个选中的乡镇
            int j = towns[i];  // 实际的乡镇编号
            
            if (ptr[i] >= town_edges[j].size()) continue;
            
            int city = town_edges[j][ptr[i]].second;
            int town_node = n + 1 + i;
            
            if (unite(town_node, city)) {
                mst_cost += w;
                edges_used++;
            }
            
            // 加入该乡镇的下一条边
            ptr[i]++;
            if (ptr[i] < town_edges[j].size()) {
                int next_w = town_edges[j][ptr[i]].first;
                pq.push({next_w, {1, i}});
            }
        }
    }
    
    // 检查所有城市是否连通
    int root = find(1);
    for (int i = 2; i <= n; i++) {
        if (find(i) != root) {
            return INF;  // 城市不连通
        }
    }
    
    return town_cost + mst_cost;
}

int main() {
    scanf("%d%d%d", &n, &m, &k);
    
    // 读取原图边
    vector<Edge> old_edges(m);
    for (int i = 0; i < m; i++) {
        scanf("%d%d%d", &old_edges[i].u, &old_edges[i].v, &old_edges[i].w);
    }
    
    // 读取乡镇信息
    for (int j = 0; j < k; j++) {
        scanf("%d", &c[j]);
        for (int i = 1; i <= n; i++) {
            int a;
            scanf("%d", &a);
            town_edges[j].push_back({a, i});
        }
        // 对每个乡镇到城市的边按权值排序
        sort(town_edges[j].begin(), town_edges[j].end());
    }
    
    // 求原图的最小生成树
    sort(old_edges.begin(), old_edges.end());
    for (int i = 1; i <= n; i++) fa[i] = i;
    
    int cnt = 0;
    for (int i = 0; i < m && cnt < n - 1; i++) {
        if (unite(old_edges[i].u, old_edges[i].v)) {
            cnt++;
            mst_edges.push_back(old_edges[i]);
        }
    }
    
    // 枚举所有乡镇选择方案
    ll ans = INF;
    
    // 先计算不选任何乡镇的情况
    for (int i = 1; i <= n; i++) fa[i] = i;
    ll base_cost = 0;
    for (const auto& e : mst_edges) {
        if (unite(e.u, e.v)) {
            base_cost += e.w;
        }
    }
    ans = min(ans, base_cost);
    
    // 枚举其他方案
    for (int mask = 1; mask < (1 << k); mask++) {
        ll cost = solve_mask(mask);
        ans = min(ans, cost);
    }
    
    printf("%lld\n", ans);
    return 0;
}

功能分析

一. 算法复杂度分析

时间复杂度

  1. 求原图MST:O(m log m),其中 m ≤ 1e6
  2. 预处理乡镇边排序:O(kn log n),其中 k ≤ 10,n ≤ 1e4
  3. 枚举所有方案: O ( 2 k × ( n + k ) × l o g k ) ,其中 2 k ≤ 1024 , l o g k ≤ 4 O(2^k × (n+k) × log k),其中 2^k ≤ 1024,log k ≤ 4 O(2k×(n+k)×logk),其中2k≤1024,logk≤4
    • 每次枚举的多路归并Kruskal复杂度为O((n+k) × log k)
    • 总复杂度约 1024 × 10000 × 4 ≈ 4× 10 7 10^7 107 次操作

空间复杂度:O(m + kn),主要用于存储边

二. 关键优化点
  1. 边集缩减:只保留原图的最小生成树边,将边数从m减少到n-1
  2. 多路归并 :避免每次枚举都对所有边完全排序
    • 原图MST边已经有序
    • 每个乡镇到城市的边预先排序
    • 使用优先队列维护当前最小边
  3. 状态压缩枚举:利用k≤10的特点,枚举所有2^k种乡镇选择方案
  4. 验证连通性:只要求所有城市连通,乡镇作为中间节点

各种学习资料,助力大家一站式学习和提升!!!

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
int main(){
	cout<<"##########  一站式掌握信奥赛知识!  ##########";
	cout<<"#############  冲刺信奥赛拿奖!  #############";
	cout<<"######  课程购买后永久学习,不受限制!   ######";
	return 0;
}

1、csp信奥赛高频考点知识详解及案例实践:

CSP信奥赛C++动态规划:
https://blog.csdn.net/weixin_66461496/category_13096895.html点击跳转

CSP信奥赛C++标准模板库STL:
https://blog.csdn.net/weixin_66461496/category_13108077.html 点击跳转

信奥赛C++提高组csp-s知识详解及案例实践:
https://blog.csdn.net/weixin_66461496/category_13113932.html

2、csp信奥赛冲刺一等奖有效刷题题解:

CSP信奥赛C++初赛及复赛高频考点真题解析(持续更新):https://blog.csdn.net/weixin_66461496/category_12808781.html 点击跳转

CSP信奥赛C++一等奖通关刷题题单及题解(持续更新):https://blog.csdn.net/weixin_66461496/category_12673810.html 点击跳转

3、GESP C++考级真题题解:

GESP(C++ 一级+二级+三级)真题题解(持续更新):https://blog.csdn.net/weixin_66461496/category_12858102.html 点击跳转

GESP(C++ 四级+五级+六级)真题题解(持续更新):https://blog.csdn.net/weixin_66461496/category_12869848.html 点击跳转

GESP(C++ 七级+八级)真题题解(持续更新):
https://blog.csdn.net/weixin_66461496/category_13117178.html

4、CSP信奥赛C++竞赛拿奖视频课:

https://edu.csdn.net/course/detail/40437 点击跳转

· 文末祝福 ·

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
int main(){
	cout<<"跟着王老师一起学习信奥赛C++";
	cout<<"    成就更好的自己!       ";
	cout<<"  csp信奥赛一等奖属于你!   ";
	return 0;
}
相关推荐
星火开发设计5 小时前
枚举类 enum class:强类型枚举的优势
linux·开发语言·c++·学习·算法·知识
qq_1927798711 小时前
C++模块化编程指南
开发语言·c++·算法
代码村新手11 小时前
C++-String
开发语言·c++
历程里程碑13 小时前
滑动窗口---- 无重复字符的最长子串
java·数据结构·c++·python·算法·leetcode·django
2501_9403152614 小时前
航电oj:首字母变大写
开发语言·c++·算法
lhxcc_fly14 小时前
手撕简易版的智能指针
c++·智能指针实现
浒畔居14 小时前
泛型编程与STL设计思想
开发语言·c++·算法
Fcy64814 小时前
C++ 异常详解
开发语言·c++·异常
机器视觉知识推荐、就业指导15 小时前
Qt 和 C++,是不是应该叫 Q++ 了?
开发语言·c++·qt