题解:洛谷 P3366 最小生成树

【题目来源】

洛谷:P3366 【模板】最小生成树 - 洛谷

【题目描述】

如题,给出一个无向图,求出最小生成树,如果该图不连通,则输出 orz

【输入】

第一行包含两个整数 \(N,M\),表示该图共有 \(N\) 个结点和 \(M\) 条无向边。

接下来 \(M\) 行每行包含三个整数 \(X_i,Y_i,Z_i\),表示有一条长度为 \(Z_i\) 的无向边连接结点 \(X_i,Y_i\)

【输出】

如果该图连通,则输出一个整数表示最小生成树的各边的长度之和。如果该图不连通则输出 orz

【输入样例】

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

【输出样例】

7

【核心思想】

  1. 问题分析:给定一个 \(N\) 个节点、\(M\) 条边的无向图,每条边有边权。需要找到一棵连接所有节点的生成树,使得树中所有边的权值之和最小。若图不连通,则不存在生成树,输出 orz。这是经典的最小生成树(MST)问题,可以通过 Prim 或 Kruskal 算法解决。

  2. 算法选择

    • Prim 算法:从任意节点开始,逐步将离当前生成树最近的节点加入,适合稠密图。堆优化版本可达到 \(O(M \log N)\)
    • Kruskal 算法:将所有边按权值从小到大排序,依次加入不形成环的边,直到树中包含 \(N-1\) 条边。适合稀疏图,复杂度 \(O(M \log M)\)
    • 并查集:Kruskal 中用于快速判断是否形成环。
  3. 关键步骤(以 Kruskal 为例):

    • 读入与存储:读取 \(N, M\),存储所有边 \((u, v, w)\)
    • 排序:按边权 \(w\) 从小到大排序。
    • 初始化并查集:每个节点自成一个集合。
    • 贪心选边:遍历排序后的边,若边的两端点不在同一集合中,则加入该边(合并集合),累加权值,计数加 1。
    • 终止条件:当选边数达到 \(N-1\) 时,生成树构建完成;若遍历完所有边仍不足 \(N-1\) 条,则图不连通。
    • 输出:连通时输出总权值,否则输出 orz
  4. 时间/空间复杂度

    • 时间复杂度:\(O(M \log M)\)(Kruskal)或 \(O(M \log N)\)(堆优化 Prim)。
    • 空间复杂度:\(O(N + M)\)
  5. 最小生成树的核心思想

    • 贪心策略:每次选择当前权值最小的边,只要不形成环,就加入生成树。这种局部最优选择能保证全局最优(由割性质和回路性质保证)。
    • 并查集的高效性:用于动态维护连通分量,快速判断边是否连接两个不同集合。
    • 适用场景:网络设计、电路布线、公路建设等最小成本连通问题。

【算法标签】

普及 #生成树

【代码详解】

#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>
#define inf 1e9
using namespace std;

const int N = 5010;        // 最大节点数

int n, m, a, b, c;         // n:节点数, m:边数, a,b,c:临时变量
int ans;                    // 最小生成树的总权重
int cnt;                    // 已加入生成树的节点数

// 边结构体
struct edge
{
    int v;                  // 目标节点
    int w;                  // 边权重
};

vector<edge> e[N];          // 邻接表存储图
int d[N];                   // 存储当前生成树到各节点的最小距离
int vis[N];                 // 标记节点是否已加入最小生成树

// 使用大根堆模拟小根堆:存储{-距离, 节点}
priority_queue<pair<int, int>> q;

/**
 * Prim算法(堆优化版):求解最小生成树
 * @param s 起始节点
 * @return true: 图连通,存在最小生成树;false: 图不连通
 */
bool prim(int s)
{
    // 初始化距离数组为无穷大
    for (int i = 0; i <= n; i++)
    {
        d[i] = inf;
    }

    // 起始节点距离设为0,并加入优先队列
    d[s] = 0;
    q.push({0, s});

    // 当队列不为空时继续处理
    while (q.size())
    {
        int u = q.top().second;  // 获取当前距离最小的节点
        q.pop();

        // 如果节点已经处理过,跳过(延迟删除)
        if (vis[u])
        {
            continue;
        }

        // 标记节点u已加入最小生成树
        vis[u] = 1;

        // 累加当前边的权重到总结果
        ans += d[u];

        // 已加入生成树的节点数加1
        cnt++;

        // 遍历节点u的所有邻接边
        for (auto ed : e[u])
        {
            int v = ed.v;    // 邻接节点
            int w = ed.w;    // 边权重

            // 如果通过u到v的距离更小,更新距离
            if (d[v] > w)
            {
                d[v] = w;   // 更新到v的最小距离

                // 将新距离加入优先队列(使用负数模拟小根堆)
                q.push({-d[v], v});
            }
        }
    }

    // 检查是否所有节点都加入了生成树(图是否连通)
    return cnt == n;
}

int main()
{
    // 输入节点数和边数
    cin >> n >> m;

    // 输入所有边信息,构建无向图
    for (int i = 0; i < m; i++)
    {
        cin >> a >> b >> c;

        // 无向图,双向添加边
        e[a].push_back({b, c});
        e[b].push_back({a, c});
    }

    // 从节点1开始执行Prim算法
    if (!prim(1))
    {
        // 图不连通,无法生成最小生成树
        puts("orz");
    }
    else
    {
        // 输出最小生成树的总权重
        printf("%d\n", ans);
    }

    return 0;
}
// 使用acwing模板二刷
#include <bits/stdc++.h>
using namespace std;

const int N = 5005;        // 最大节点数
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);
        // 如果数据中没有重边,可以使用下面这行:
        // g[a][b] = g[b][a] = c;
    }

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

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

    return 0;
}
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 200006;        // 最大边数

int n;                       // 节点数量
int m;                       // 边数量
int ans;                     // 最小生成树的总权重
int cnt;                     // 已选择的边数

// 边结构体
struct edge
{
    int u, v, w;            // u:起点, v:终点, w:权重

    // 重载小于运算符,用于按权重排序
    bool operator<(const edge &t) const
    {
        return w < t.w;
    }
} e[N];                     // 边数组

int fa[N];                   // 并查集数组,用于记录节点的父节点

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

/**
 * Kruskal算法:求解最小生成树
 * @return true: 图连通,存在最小生成树;false: 图不连通
 */
bool kruskal()
{
    // 将边按权重从小到大排序
    sort(e, e + m);

    // 初始化并查集,每个节点独立成集合
    for (int i = 1; i <= n; i++)
    {
        fa[i] = i;
    }

    // 遍历所有边(按权重从小到大)
    for (int i = 0; i < m; i++)
    {
        // 查找两个端点的根节点
        int x = find(e[i].u);
        int y = find(e[i].v);

        // 如果两个端点不在同一个连通分量中
        if (x != y)
        {
            fa[x] = y;          // 合并两个连通分量
            ans += e[i].w;      // 累加当前边的权重
            cnt++;              // 已选边数加1
        }
    }

    // 最小生成树应有n-1条边,检查图是否连通
    return cnt == n - 1;
}

int main()
{
    // 输入节点数和边数
    cin >> n >> m;

    // 输入所有边的信息
    for (int i = 0; i < m; i++)
    {
        cin >> e[i].u >> e[i].v >> e[i].w;
    }

    // 调用Kruskal算法计算最小生成树
    if (!kruskal())
    {
        // 图不连通,无法生成最小生成树
        puts("orz");
    }
    else
    {
        // 输出最小生成树的总权重
        printf("%d\n", ans);
    }

    return 0;
}
// 使用acwing模板二刷
#include <bits/stdc++.h>
using namespace std;

const int N = 5005;        // 最大节点数
const int M = 200005;       // 最大边数
const int INF = 1e9;        // 无穷大值(实际未使用)

int n;                      // 节点数量
int m;                      // 边数量
int ans;                    // 最小生成树的总权重
int cnt;                    // 已选择的边数
int p[N];                   // 并查集数组,用于记录节点的父节点

// 边结构体
struct Edge
{
    int a, b, w;           // a:起点, b:终点, w:权重

    // 重载小于运算符,用于按权重排序
    bool operator < (const Edge &E) const
    {
        return w < E.w;
    }
} edges[M];                 // 边数组

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

int main()
{
    // 输入节点数和边数
    cin >> n >> m;

    // 输入所有边的信息
    for (int i = 1; i <= m; i++)
    {
        int a, b, w;
        cin >> a >> b >> w;

        // 确保起点编号小于终点编号(可选操作,便于调试)
        if (a > b)
        {
            swap(a, b);
        }

        // 存储边信息
        edges[i] = {a, b, w};
    }

    // 将边按权重从小到大排序
    sort(edges + 1, edges + m + 1);

    // 初始化并查集,每个节点独立成集合
    for (int i = 1; i <= n; i++)
    {
        p[i] = i;
    }

    // 使用Kruskal算法构建最小生成树
    for (int i = 1; i <= m; i++)
    {
        int s = edges[i].a;    // 当前边的起点
        int e = edges[i].b;    // 当前边的终点
        int w = edges[i].w;    // 当前边的权重

        // 查找两个端点的根节点
        s = find(s);
        e = find(e);

        // 如果两个端点不在同一个连通分量中
        if (s != e)
        {
            p[e] = s;         // 合并两个连通分量
            ans += w;         // 累加当前边的权重
            cnt++;            // 已选边数加1
        }
    }

    // 检查图是否连通:最小生成树应有n-1条边
    if (cnt < n - 1)
    {
        // 图不连通,无法生成最小生成树
        cout << "orz" << endl;
    }
    else
    {
        // 输出最小生成树的总权重
        cout << ans << endl;
    }

    return 0;
}

【运行结果】

4 5
1 2 2
1 3 2
1 4 3
2 3 4
3 4 3
7
posted @ 2026-09-01 16:52  团爸讲算法  阅读(20)  评论(0)    收藏  举报