题解:AcWing 858 Prim算法求最小生成树

【题目来源】

AcWing:858 Prim算法求最小生成树 - AcWing题库

【题目描述】

给定一个 \(n\) 个点 \(m\) 条边的无向图,图中可能存在重边和自环,边权可能为负数。

求最小生成树的树边权重之和,如果最小生成树不存在则输出 impossible

给定一张边带权的无向图 \(G=(V,E)\),其中 \(V\) 表示图中点的集合,\(E\) 表示图中边的集合,\(n=|V|\)\(m=|E|\)

\(V\) 中的全部 \(n\) 个顶点和 \(E\)\(n-1\) 条边构成的无向连通子图被称为 \(G\) 的一棵生成树,其中边的权值之和最小的生成树被称为无向图 \(G\) 的最小生成树。

【输入】

第一行包含两个整数 \(n\)\(m\)

接下来 \(m\) 行,每行包含三个整数 \(u,v,w\),表示点 \(u\) 和点 \(v\) 之间存在一条权值为 \(w\) 的边。

【输出】

共一行,若存在最小生成树,则输出一个整数,表示最小生成树的树边权重之和,如果最小生成树不存在则输出 impossible

【输入样例】

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

【输出样例】

6

【核心思想】

  1. 问题分析:给定无向图 \(G=(V,E)\),其中 \(|V|=n\)\(|E|=m\),边权可能为负数,图中可能存在重边和自环。需要求最小生成树的树边权重之和,若图不连通则输出 impossible。这是一个最小生成树(MST)问题,可以使用 Prim 算法求解。

  2. 算法选择

    • Prim 算法(朴素版):从单个节点开始,逐步扩展生成树,每次选择距离当前生成树最近的节点加入
    • 贪心策略:每次选择权值最小的边连接生成树内外的节点,保证局部最优达到全局最优
    • 邻接矩阵存储:适用于稠密图,空间复杂度 \(O(n^2)\)
    • 距离数组 \(dist[]\)\(dist[i]\) 表示节点 \(i\) 到当前生成树的最小距离(即连接生成树的最小边权)
    • 标记数组 \(st[]\):标记节点是否已加入最小生成树
  3. 关键步骤

    • 初始化
      • 读取 \(n\)(节点数)、\(m\)(边数)
      • 初始化邻接矩阵 \(g[i][j] = INF\),处理重边时取最小值 g[a][b] = g[b][a] = min(g[a][b], c)
      • 初始化距离数组 \(dist[i] = INF\),标记数组 \(st[i] = false\)
    • Prim 主循环(循环 \(n\) 次,每次加入一个节点):
      • 找最小节点:在未加入生成树的节点中,找到 \(dist\) 值最小的节点 \(t\)
      • 连通性判断:若 \(i > 0\)\(dist[t] == INF\),说明图不连通,返回 impossible
      • 累加答案:若 \(i > 0\),将 \(dist[t]\) 加入总权重 \(res\)
      • 更新距离:用新加入的节点 \(t\) 更新其他节点到生成树的最小距离:dist[j] = min(dist[j], g[t][j])
      • 标记加入\(st[t] = true\)
    • 返回结果:输出最小生成树总权重 \(res\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(n^2)\),每次找最小节点 \(O(n)\),更新距离 \(O(n)\),共 \(n\)
    • 空间复杂度:\(O(n^2)\),邻接矩阵存储
    • 适用于稠密图(\(m\) 接近 \(n^2\)),稀疏图建议使用 Kruskal 算法
  5. Prim 算法的核心思想

    • 贪心策略:每次选择权值最小的边连接生成树内外的节点,保证全局最优
    • 集合扩展:从一个节点开始,逐步将距离生成树最近的节点加入,直到包含所有节点
    • 距离维护\(dist[i]\) 始终维护节点 \(i\) 到当前生成树的最小连接边权
    • 稠密图优势:使用邻接矩阵时,Prim 算法在稠密图上效率高于 Kruskal
    • 正确性证明:基于割性质,最小生成树必然包含连接割两侧的最小权边
    • 适用于稠密图、需要逐步构建生成树的场景

【解题思路】

【算法标签】

生成树

【代码详解】

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

const int N = 510;        // 最大节点数
const int INF = 0x3f3f3f3f; // 无穷大值

int n;                     // 节点数量
int m;                     // 边数量
int g[N][N];              // 邻接矩阵存储图
int dist[N];              // 存储当前生成树到各节点的最小距离
bool st[N];               // 标记节点是否已加入最小生成树

/**
 * Prim算法:求解无向连通图的最小生成树
 * @return 最小生成树的总权重,INF表示图不连通
 */
int prim()
{
    // 初始化距离数组为无穷大
    memset(dist, 0x3f, sizeof(dist));

    int res = 0;           // 存储最小生成树的总权重

    // 循环n次,每次加入一个节点到生成树
    for (int i = 0; i < n; i++)
    {
        int t = -1;        // 用于记录当前距离最小的节点

        // 在所有未加入生成树的节点中,找到距离最小的节点
        for (int j = 1; j <= n; j++)
        {
            if (!st[j] && (t == -1 || dist[t] > dist[j]))
            {
                t = j;
            }
        }

        // 如果不是第一个节点(i>0)且最小距离为无穷大,说明图不连通
        if (i && dist[t] == INF)
        {
            return INF;    // 图不连通,无法生成最小生成树
        }

        // 如果不是第一个节点,将当前边的权重加入总结果
        if (i)
        {
            res += dist[t];
        }

        // 用新加入的节点t更新其他节点到生成树的最小距离
        for (int j = 1; j <= n; j++)
        {
            dist[j] = min(dist[j], g[t][j]);
        }

        // 标记节点t已加入最小生成树
        st[t] = true;
    }

    return res;
}

int main()
{
    // 输入节点数和边数
    scanf("%d%d", &n, &m);

    // 初始化邻接矩阵为无穷大(表示不可达)
    memset(g, 0x3f, sizeof(g));

    // 输入所有边的信息
    while (m--)
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);

        // 无向图,双向设置权重,并处理重边(取最小值)
        g[a][b] = g[b][a] = min(g[a][b], c);
    }

    // 调用Prim算法计算最小生成树
    int t = prim();

    // 输出结果
    if (t == INF)
    {
        // 图不连通,无法生成最小生成树
        puts("impossible");
    }
    else
    {
        // 输出最小生成树的总权重
        printf("%d\n", t);
    }

    return 0;
}

【运行结果】

4 5
1 2 1
1 3 2
1 4 3
2 3 2
3 4 4
6
posted @ 2026-06-16 10:15  团爸讲算法  阅读(10)  评论(0)    收藏  举报