1

GESP认证C++编程真题解析 | 202509 八级

欢迎大家订阅我的CSDN专栏算法题解:C++与Python实现
本专栏旨在帮助大家从基础到进阶 ,逐步提升编程能力,助力信息学竞赛备战!

专栏特色
1.经典算法练习:根据信息学竞赛大纲,精心挑选经典算法题目,提供清晰的代码实现与详细指导,帮助您夯实算法基础。
2.系统化学习路径:按照算法类别和难度分级,从基础到进阶,循序渐进,帮助您全面提升编程能力与算法思维。

适合人群:

  • 准备参加蓝桥杯、GESP、CSP-J、CSP-S等信息学竞赛的学生
  • 希望系统学习C++/Python编程的初学者
  • 想要提升算法与编程能力的编程爱好者

附上汇总帖:GESP认证C++编程真题解析 | 汇总


编程题

P14080 最小生成树

【题目来源】

洛谷:[P14080 GESP202509 八级] 最小生成树 - 洛谷

【题目描述】

给定一张包含 \(n\) 个结点 \(m\) 条边的带权连通无向图,结点依次以 \(1,2,\ldots,n\) 编号,第 \(i\) 条边(\(1\le i\le m\))连接结点 \(u_i\) 与结点 \(v_i\),边权为 \(w_i\)

对于每条边,请你求出从图中移除该条边后,图的最小生成树中所有边的边权和。特别地,若移除某条边后图的最小生成树不存在,则输出 \(-1\)

【输入】

第一行,两个正整数 \(n,m\),分别表示图的结点数与边数。

接下来 \(m\) 行中的第 \(i\) 行(\(1\le i\le m\))包含三个正整数 \(u_i,v_i,w_i\),表示图中连接结点 \(u_i\) 与结点 \(v_i\) 的边,边权为 \(w_i\)

【输出】

输出共 \(m\) 行,第 \(i\) 行(\(1\le i\le m\))包含一个整数,表示移除第 \(i\) 条边后,图的最小生成树中所有边的边权和。若移除第 \(i\) 条边后图的最小生成树不存在,则输出 \(−1\)

【输入样例】

5 5
1 2 4
2 3 3
3 4 1
2 5 2
3 1 8

【输出样例】

14
15
-1
-1
10

【解题思路】

image

【算法标签】

《洛谷 P14080 最小生成树》 #线段树# #倍增# #并查集# #生成树# #最近公共祖先LCA# #树链剖分# #GESP# #2025#

【代码详解】

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

#define int long long  // 使用长整型防止溢出
const int N = 100005;   // 最大节点数和边数

// 边结构体
struct Edge
{
    int a, b, w, idx;   // a:起点, b:终点, w:权重, idx:边的编号
    
    // 重载小于运算符,按权重排序
    bool operator< (const Edge &t) const
    {
        return w < t.w;
    }
} e[N], nt[N];          // e:所有边, nt:非树边(不在最小生成树中的边)

int n, m;               // n:节点数, m:边数
int u, v, w, idx;       // 临时变量
int cur;                // 非树边计数器
int ans;                // 最小生成树的总权重
int ids[N];             // 存储每个节点到根路径上的最大非树边权重
int p[N];               // 并查集数组(用于Kruskal算法)
int fa[N];              // 存储每个节点的父节点(用于DFS树)
int deep[N];            // 存储每个节点的深度
bool etree[N];          // 标记边是否在最小生成树中
vector<Edge> g[N];      // 最小生成树的邻接表

/**
 * 并查集查找操作(带路径压缩)
 * @param x 要查找的节点
 * @return 节点x的根节点
 */
int find(int x)
{
    if (p[x] != x)
    {
        p[x] = find(p[x]);  // 路径压缩
    }
    return p[x];
}

/**
 * DFS遍历最小生成树,构建父节点和深度信息
 * @param fx 父节点
 * @param x 当前节点
 * @param d 当前深度
 */
void dfs(int fx, int x, int d)
{
    if (fx == 0)
    {
        fa[x] = x;        // 根节点的父节点指向自己
    }
    else
    {
        fa[x] = fx;       // 记录父节点
    }
    deep[x] = d;          // 记录深度
    
    // 遍历所有邻接边
    for (int i = 0; i < g[x].size(); i++)
    {
        // 跳过父节点,避免回边
        if (g[x][i].b == fx)
        {
            continue;
        }
        dfs(x, g[x][i].b, d + 1);
    }
}

/**
 * 比较函数:按边的原始编号排序
 */
bool cmp(Edge x, Edge y)
{
    return x.idx < y.idx;
}

signed main()  // 使用signed代替int(因为定义了#define int long long)
{
    // 输入节点数和边数
    cin >> n >> m;
    
    // 输入所有边的信息
    for (int i = 1; i <= m; i++)
    {
        cin >> e[i].a >> e[i].b >> e[i].w;
        e[i].idx = i;  // 记录边的原始编号
    }
    
    // ========== Kruskal算法构建最小生成树 ==========
    // 初始化并查集
    for (int i = 1; i <= n; i++)
    {
        p[i] = i;
    }
    
    // 将边按权重从小到大排序
    sort(e + 1, e + m + 1);
    
    // 构建最小生成树
    for (int i = 1; i <= m; i++)
    {
        int a = e[i].a, b = e[i].b, w = e[i].w, idx = e[i].idx;
        
        // 如果两个节点不在同一个连通分量中
        if (find(a) != find(b))
        {
            // 合并两个连通分量
            p[find(a)] = find(b);
            
            // 标记该边在最小生成树中
            etree[idx] = 1;
            
            // 将边添加到最小生成树的邻接表中
            g[a].push_back({0, b, w, idx});
            g[b].push_back({0, a, w, idx});
            
            // 累加最小生成树的总权重
            ans += w;
        }
        else
        {
            // 该边不在最小生成树中,记录为非树边
            nt[++cur] = e[i];
            continue;
        }
    }
    
    // 检查图是否连通
    for (int i = 1; i <= n; i++)
    {
        if (find(1) != find(i))
        {
            // 图不连通,输出-1
            for (int j = 1; j <= m; j++)
            {
                cout << -1 << endl;
            }
            return 0;
        }
    }
    
    // ========== 预处理每个节点到根路径上的最大非树边权重 ==========
    // 重新初始化并查集
    for (int i = 1; i <= n; i++)
    {
        p[i] = i;
        ids[i] = -1;  // 初始化为-1,表示未知
    }
    
    // 从节点1开始DFS遍历最小生成树,构建父节点和深度信息
    dfs(0, 1, 1);
    
    // 处理所有非树边,记录路径上的最大权重
    for (int i = 1; i <= cur; i++)
    {
        int a = find(nt[i].a), b = find(nt[i].b), w = nt[i].w;
        
        // 找到两个节点在树上的LCA路径
        while (a != b)
        {
            // 选择深度较大的节点向上跳
            if (deep[a] < deep[b])
            {
                swap(a, b);
            }
            
            // 记录该节点路径上的最大非树边权重
            ids[a] = w;
            
            // 将当前节点合并到父节点所在的集合
            p[a] = find(fa[a]);
            a = find(a);
        }
    }
    
    // ========== 计算次小生成树 ==========
    // 将边按原始编号排序,以便按输入顺序输出
    sort(e + 1, e + m + 1, cmp);
    
    // 对每条边计算次小生成树权重
    for (int i = 1; i <= m; i++)
    {
        if (!etree[i])
        {
            // 非树边:次小生成树权重等于最小生成树权重
            cout << ans << endl;
        }
        else
        {
            // 树边:需要替换该边
            int a = e[i].a, b = e[i].b, w = e[i].w;
            
            // 确保a是深度较大的节点(子节点)
            if (deep[a] < deep[b])
            {
                swap(a, b);
            }
            
            if (ids[a] == -1)
            {
                // 没有可替换的边,无法形成次小生成树
                cout << -1 << endl;
            }
            else
            {
                // 计算次小生成树权重:最小生成树权重 - 当前边权重 + 替换边权重
                cout << ans - w + ids[a] << endl;
            }
        }
    }
    
    return 0;
}

【运行结果】

5 5
1 2 4
2 3 3
3 4 1
2 5 2
3 1 8
14
15
-1
-1
10
posted @ 2026-01-15 09:26  热爱编程的通信人  阅读(4)  评论(0)    收藏  举报