最小生成树算法

目录

一、前置知识:什么是最小生成树?

[二、Kruskal 算法("边"贪心,稀疏图首选)](#二、Kruskal 算法(“边”贪心,稀疏图首选))

[1. 核心思想](#1. 核心思想)

[2. 算法步骤](#2. 算法步骤)

[3. 完整代码 + 逐行注释](#3. 完整代码 + 逐行注释)

[4. 算法优缺点](#4. 算法优缺点)

[三、Prim 算法("点"贪心,稠密图首选)](#三、Prim 算法(“点”贪心,稠密图首选))

[1. 核心思想](#1. 核心思想)

[2. 算法步骤](#2. 算法步骤)

[3. 完整代码 + 逐行注释](#3. 完整代码 + 逐行注释)

[4. 算法优缺点](#4. 算法优缺点)

[四、两大算法核心对比(面试 / 做题必背)](#四、两大算法核心对比(面试 / 做题必背))

五、做题小技巧

六、总结

关键点回顾


最小生成树(Minimum Spanning Tree,MST)是图论中最经典的算法之一,核心作用是:在一个带权无向连通图中,选出 n-1 条边,连接所有 n 个节点,且总边权最小(不形成环)。

本文将带你吃透最小生成树的两大核心算法:

✅ Kruskal 算法(并查集 + 贪心,适合稀疏图)

✅ Prim 算法(贪心 + 遍历,适合稠密图)

附带可直接运行的 C++ 模板 + 详细注释 + 场景对比,新手也能轻松掌握!


一、前置知识:什么是最小生成树?

举个生活例子:你要给 n 个村庄修公路,任意两个村庄之间修公路的成本不同,要求所有村庄连通总修路成本最低不修环路 (浪费钱),最终修出来的公路网,就是这个图的最小生成树

核心性质:

  1. 包含图中所有 n 个节点
  2. 有且仅有n-1 条边
  3. 总边权最小
  4. 无环、连通

这也是最小生成树名字的由来:只需保证联通无需任意两点间都有路(树),在这基础上选择成本最低的方案(最小)

但是要注意在实际应用中,生成树是成本最小不代表最短,在实际中我们要兼顾路程和成本,比如贵州花江峡谷大桥可能不是联通两地成本最低的方案,但是建造之后所带来的效益却是远超其他方案

算法不要死学,学的是计算机思维


二、Kruskal 算法("边"贪心,稀疏图首选)

1. 核心思想

对边进行操作,图中的边权最小的边定然是生成树的一条枝干,这便是 Kruskal 算法最核心的贪心思想,对边从小到大遍历,只要边所连的两点不在同一个联通块,就保留这条边,最终选择n-1条边(节点数为n的树边为n-1)

一句话总结:小边优先,并查集判环

2. 算法步骤

  1. 把图中所有边按权值升序排列(使用优先队列贼方便)
  2. 初始化并查集(每个节点自己是一个连通块)
  3. 遍历排序后的边:
    • 若边的两个端点不属于同一连通块 → 选中这条边,合并连通块
    • 若属于同一连通块 → 跳过(会成环)
  4. 选中 n-1 条边时,结束算法

3. 完整代码 + 逐行注释

P3366 【模板】最小生成树https://www.luogu.com.cn/problem/P3366

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 2e6 + 10;
const int inf = 2147483647;

// 边的结构体:存储两个端点+边权
struct edge {
    int x, y, z;
};

// 优先队列比较器:小顶堆(边权小的优先出队)
struct cmp {
    bool operator()(const edge &e1, const edge &e2) {
        return e1.z > e2.z;
    }
};

int fa[N];       // 并查集父节点数组
int n, m;        // n:节点数,m:边数
int sum, cnt;    // sum:最小生成树总权值,cnt:已选边数
priority_queue<edge, vector<edge>, cmp> q; // 存储所有边

// 并查集初始化:每个节点的父节点是自己
void init(){
    for(int i = 0; i <= n; i++) fa[i] = i;
}

// 并查集查找
int get(int x){
    return fa[x] = (fa[x] == x ? x : get(fa[x]));
}

// 合并两个连通块,a挂在b上
void merge(int a, int b){
    fa[get(a)] = get(b);
}

// Kruskal算法核心
void kruskal() {
    while (!q.empty() && cnt < n-1) {
        edge t = q.top(); q.pop(); // 取出当前最小边
        int x = t.x, y = t.y, z = t.z;
        
        // 两个节点不在同一连通块 → 选中这条边
        if (get(x) != get(y)) {
            sum += z;    // 累加总权值
            merge(x, y); // 合并连通块
            cnt++;       // 已选边数+1
        }
    }
    // 最终判断:是否连通(选够n-1条边)
    if (cnt == n-1) cout << sum << endl;
    else cout << "orz" << endl; // 图不连通,无生成树
}

void solve() {
    cin >> n >> m;
    init();
    sum = 0, cnt = 0; // 初始化变量
    for (int i = 0; i < m; i++) {
        int x, y, z;
        cin >> x >> y >> z;
        q.push({x, y, z}); // 边存入优先队列
    }
    kruskal();
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    solve();
    return 0;
}

4. 算法优缺点

通过上面的讲解大家应该看出来了,Kruskal 算法一直都是在对边进行操作,故而:

✅ 优点:代码简洁、易理解、稀疏图效率高(边少的图)

❌ 缺点:需要存储所有边,稠密图(边多)效率略低

时间复杂度:O(mlogm)(m 为边数,主要耗时在排序)


三、Prim 算法("点"贪心,稠密图首选)

1. 核心思想

核心贪心思想和 Kruskal 算法差不多,只不过Prim算法是对点进行操作,Prim需要抽象出两个集合,集合1:已选节点集合(也就是已经确定是树的一部分),集合2:未选节点集合(待成树),每次选2到1的最小边权,将对应节点加入集合,直到所有节点都被选中。我们不用在意具体两点间的边权,考虑的是点到集合1的最短距离(看后面代码理解)

一句话总结:点集扩张,每次选最短跨集边

2. 算法步骤

  1. 初始化:选定一个起点(如节点 1),标记为已选
  2. 维护数组d[]d[i]表示节点 i 到已选点集的最小距离
  3. 循环 n-1 次(需要选 n-1 条边):
    • 找到未选节点中d[]最小的节点,加入已选集合
    • 累加边权,更新其他未选节点的d[]
  4. 所有节点加入后,结束算法

3. 完整代码 + 逐行注释

P3366 【模板】最小生成树https://www.luogu.com.cn/problem/P3366朴素版

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 2e6 + 10;
const int inf = 2147483647;

// 邻接表节点:存储终点+边权
struct node {
    int u, w;
};

bool st[N];       // 标记节点是否已加入生成树
vector<node> mp[N]; // 邻接表:存储图
int d[N];         // d[i]:节点i到已选点集的最小距离
int n, m, sum;    // sum:最小生成树总权值

// Prim算法核心
void prim() {
    d[1] = 0;        // 从节点1开始,初始距离为0
    st[1] = true;    // 标记节点1已选
    
    // 初始化:起点的邻接节点距离
    for (auto x : mp[1]) d[x.u] = min(d[x.u], x.w);
    
    // 选n-1条边,循环n-1次
    for (int i = 2; i <= n; i++) {
        int mn = inf, xb = -1;
        // 找到未选节点中,d[]最小的节点
        for (int j = 2; j <= n; j++) {
            if (!st[j] && d[j] < mn) {
                mn = d[j];
                xb = j;
            }
        }
        
        // 无符合条件的节点 → 图不连通,直接结束即可!
        if (xb == -1) {
            sum = inf;
            return;
        }
        
        st[xb] = true;  // 标记节点已选
        sum += mn;      // 累加总权值
        
        // 更新邻接节点的最小距离
        for (auto x : mp[xb]) {
            d[x.u] = min(d[x.u], x.w);
        }
    }
}

void solve() {
    cin >> n >> m;
    for (int i = 0; i < m; i++) {
        int x, y, z;
        cin >> x >> y >> z;
        mp[x].push_back({y, z}); // 无向图,双向存边
        mp[y].push_back({x, z});
    }
    // 初始化距离数组为无穷大
    for (int i = 1; i <= n; i++) d[i] = inf;
    sum = 0;
    prim();
    
    if (sum == inf) cout << "orz" << endl;
    else cout << sum << endl;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    solve();
    return 0;
}

堆优化版

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 2e6 + 10;
const int inf = 2147483647;  // 无穷大

// 邻接表存储边:u是终点,w是边权
struct node {
    int u, w;
};

// 堆节点:存储【当前节点到生成树的最小距离】+【节点编号】
struct HeapNode {
    int dis, id;
    // 小根堆重载:让优先队列从小到大弹出(距离越小越优先)
    bool operator<(const HeapNode &other) const {
        return dis > other.dis;
    }
};

bool st[N];        // 标记节点是否已经加入最小生成树
vector<node> mp[N];// 邻接表存图
int d[N];          // d[i]:节点i到已选生成树的最小边权
int n, m, sum;     // n点数,m边数,sum生成树总权值

void prim() {
    // 1. 初始化:所有节点到生成树的距离设为无穷大
    for (int i = 1; i <= n; i++) d[i] = inf;
    // 初始化所有节点未被访问
    memset(st, 0, sizeof st);
    sum = 0;  // 总权值归零

    d[1] = 0;  // 从 1 号节点开始,初始距离为 0
    // 小根堆:每次取出【距离最小】的节点
    priority_queue<HeapNode> q;
    q.push({0, 1});  // 将起点(距离0,编号1)推入堆

    int cnt = 0;     // 记录已经加入生成树的节点数量

    // 2. 开始循环:堆不为空就继续
    while (!q.empty()) {
        // 取出堆顶:当前距离最小的节点
        auto now = q.top();
        q.pop();

        int u = now.id;   // 当前节点编号
        int dis = now.dis;// 当前节点到生成树的最小距离

        // 3. 重要剪枝:如果这个节点已经加入过生成树,直接跳过(堆里的旧数据)
        if (st[u]) continue;

        // 4. 标记该节点已加入生成树
        st[u] = true;
        sum += dis;        // 累加这条边的权值
        cnt++;             // 已加入节点数 +1

        // 5. 遍历当前节点的所有邻边(松弛操作)
        for (auto edge : mp[u]) {
            int v = edge.u;  // 邻接点编号
            int w = edge.w;  // 这条边的权值

            // 6. 如果邻接点没被选过,且新边权更小 → 更新距离
            if (!st[v] && d[v] > w) {
                d[v] = w;                // 更新最小距离
                q.push({d[v], v});       // 新状态推入堆
            }
        }
    }

    // 7. 最终判断:如果加入的节点数 != 总点数 → 图不连通
    if (cnt != n) sum = inf;
}

void solve() {
    cin >> n >> m;
    // 读入 m 条无向边,存入邻接表
    for (int i = 0; i < m; i++) {
        int x, y, z;
        cin >> x >> y >> z;
        mp[x].push_back({y, z});
        mp[y].push_back({x, z});
    }
    // 执行堆优化 Prim
    prim();
    // 输出答案:不连通输出 orz,否则输出总权值
    if (sum == inf) cout << "orz\n";
    else cout << sum << "\n";
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    solve();
    return 0;
}

4. 算法优缺点

Kruskal 算法一直都是在对边进行操作相对Prim一直在对点进行操作,故而:

✅ 优点:稠密图效率极高(边多、节点少的图),无需存储所有边

❌ 缺点:朴素版代码理解难度略高于 Kruskal

时间复杂度:朴素版O(n2),堆优化版O(mlogn)(n 为节点数)


四、两大算法核心对比(面试 / 做题必背)

维度 Kruskal 算法 Prim 算法
核心思路 边贪心,从小到大选边 点贪心,扩张点集
数据结构 优先队列 / 排序 + 并查集 邻接表 + 距离数组
判环方式 并查集(判断连通块) 无需判环(点集天然无环)
适用场景 稀疏图(边少,m 远小于 n²) 稠密图(边多,m 接近 n²)
代码难度 简单,新手首选 中等
时间复杂度 O(mlogm) 朴素版O(n2)

五、做题小技巧

  1. 稀疏图:无脑用 Kruskal(代码短,不易错)
  2. 稠密图:用 Prim 朴素版(效率拉满)
  3. 数据范围:节点数≤1e4 用 Prim,边数≤1e6 用 Kruskal

六、总结

最小生成树的本质就是贪心

  • Kruskal:选最小的边,不环就留,靠并查集实现;
  • Prim:扩最小的点,逐步连通,靠距离数组实现。

关键点回顾

  1. 最小生成树:连通所有节点、n-1 条边、总权最小、无环
  2. Kruskal:边排序 + 并查集,稀疏图首选,O(mlogm)
  3. Prim:点集扩张 + 距离数组,稠密图首选,O(n2)
  4. 核心都是贪心,根据图的疏密选择算法即可

本篇文章主要介绍两个最小生成树算法,里面有一些比如优先队列的重载并没有讲,后续还会写专门介绍优先队列使用方法

相关推荐
H_老邪2 小时前
贪心算法的应用
算法·ios·贪心算法
葳_人生_蕤2 小时前
Hot100——739.每日温度
数据结构·算法
Elsa️7462 小时前
洛谷p1046:用一个题练习排序+二分查找
c++·算法
木二_2 小时前
056.Kubernetes cert-manager Root CA自签实战
算法·容器·kubernetes
老赵聊算法、大模型备案2 小时前
网信办公示 2026 年 1-2 月生成式 AI 备案登记情况:新增 94 款,累计突破 1200 款
人工智能·算法·安全·aigc
x_xbx2 小时前
LeetCode:21. 合并两个有序链表
算法·leetcode·链表
2501_945423542 小时前
C++与Rust交互编程
开发语言·c++·算法
我能坚持多久2 小时前
【初阶数据结构10】——链式二叉树的功能实现
数据结构·算法
tankeven2 小时前
HJ131 数独数组
c++·算法