Skip to content

Kruskal 算法

稀疏图的最小生成树

Kruskal 的思路极其简单:把所有边按权值从小到大排序,依次加入,只要不形成回路就保留。当加够 V-1 条边时,最小生成树就构建完成了。它天然适合稀疏图——边少的时候排序快。

408 考研中,Kruskal 与 Prim 算法常以对比形式出题,是图论章节的核心考点。

核心思想

Kruskal 算法的核心策略是按边贪心

  • 对所有边按权值升序排序
  • 依次选择权值最小的边,若该边加入后不会形成回路,则将其纳入生成树
  • 利用并查集判断回路:若一条边的两个端点已属于同一连通分量,则加入该边会形成回路,应跳过
  • 重复上述过程,直到选够 n-1 条边(n 为顶点数),算法结束

算法过程示意:

原始边集(按权值排序):
(A,B,1) → (C,D,2) → (A,C,3) → (B,D,4) → (B,C,5)

逐步选边:
第1步: 选 (A,B,1)  ✓ 不构成回路
第2步: 选 (C,D,2)  ✓ 不构成回路
第3步: 选 (A,C,3)  ✓ 不构成回路 → 已选 n-1=3 条边,算法结束

交互可视化

通过下方的交互动画,你可以逐步观察Kruskal 算法的执行过程:

加载可视化中...

操作详解

算法思路

  1. 将图中所有边按权值升序排序
  2. 初始化并查集,每个顶点各自为一个独立集合
  3. 依次遍历排序后的边 (u, v, w):
    • 若 u 和 v 不在同一集合(即加入不会成环),则选中该边,合并两个集合
    • 若 u 和 v 在同一集合,跳过该边
  4. 当选中的边数达到 n-1 时,最小生成树构建完成

执行过程详解

以下为 Kruskal 算法的 C 语言实现:

c
#include <stdio.h>
#include <stdlib.h>

#define MAXE 1000  // 最大边数
#define MAXV 100   // 最大顶点数

typedef struct {
    int u, v;   // 边的两个端点
    int w;      // 边的权值
} Edge;

Edge edges[MAXE];  // 边集数组
int parent[MAXV];  // 并查集父节点数组
int rank_[MAXV];   // 并查集秩(用于按秩合并)

// 比较函数,用于 qsort 按权值升序排序
int cmp(const void *a, const void *b) {
    return ((Edge *)a)->w - ((Edge *)b)->w;
}

// 并查集:查找根节点(带路径压缩)
int Find(int x) {
    if (parent[x] != x)
        parent[x] = Find(parent[x]);
    return parent[x];
}

// 并查集:合并两个集合(按秩合并)
void Union(int x, int y) {
    int rx = Find(x), ry = Find(y);
    if (rx == ry) return;
    if (rank_[rx] < rank_[ry])
        parent[rx] = ry;
    else if (rank_[rx] > rank_[ry])
        parent[ry] = rx;
    else {
        parent[ry] = rx;
        rank_[rx]++;
    }
}

// Kruskal 算法
int Kruskal(int n, int m) {  // n: 顶点数, m: 边数
    int i, cnt = 0, totalW = 0;

    // 1. 按权值排序所有边
    qsort(edges, m, sizeof(Edge), cmp);

    // 2. 初始化并查集
    for (i = 0; i < n; i++) {
        parent[i] = i;
        rank_[i] = 0;
    }

    // 3. 贪心选边
    for (i = 0; i < m && cnt < n - 1; i++) {
        int u = edges[i].u, v = edges[i].v;
        if (Find(u) != Find(v)) {      // 不在同一集合,不会成环
            Union(u, v);                // 合并
            totalW += edges[i].w;       // 累加权值
            cnt++;                      // 已选边数 +1
        }
    }

    if (cnt < n - 1) return -1;  // 图不连通,无法生成 MST
    return totalW;
}

并查集辅助

并查集(Union-Find)是 Kruskal 算法高效判环的关键数据结构。

核心操作

操作功能优化策略
Find(x)查找 x 所在集合的根路径压缩:查找时将沿途节点直接挂到根上
Union(x, y)合并 x 和 y 所在的集合按秩合并:将矮树挂到高树上,控制树高

为什么用并查集判环? 若边 (u, v) 的两个端点已在同一集合中,说明 u 到 v 之间已存在路径,再加入该边必然形成回路。

经过路径压缩和按秩合并优化后,单次 Find/Union 操作的均摊时间复杂度接近 O(α(n)),其中 α 为反阿克曼函数,实际可视为常数。

复杂度分析

算法各步骤的时间消耗:

步骤时间复杂度说明
边排序O(ElogE)快排 / 归并排序
初始化并查集O(V)每个顶点初始化为独立集合
遍历边 + 并查集操作O(E·α(V))α(V) 近似为常数
总计O(ElogE)瓶颈在排序步骤

注:由于 E ≤ V(V-1)/2,所以 O(ElogE) = O(ElogV),两种写法等价。

复杂度分析

指标复杂度说明
时间复杂度O(ElogE)排序为主要开销
空间复杂度O(V+E)边集数组 O(E) + 并查集 O(V)

适用场景:Kruskal 算法的时间复杂度只与边数 E 相关,因此特别适合边稀疏图(E 远小于 V²)。

Kruskal vs Prim 对比

对比项KruskalPrim
策略按边贪心按顶点扩展
数据结构并查集优先队列 / 邻接矩阵
时间复杂度O(ElogE)O(V²) 或 O(ElogV)
适用图边稀疏图边稠密图
边的存储边集数组邻接矩阵 / 邻接表

考研高频考点

  • ⭐ Kruskal 算法的执行过程手动模拟(选择题/应用题高频)
  • ⭐ Kruskal vs Prim 的适用场景对比(简答题必考)
  • ⭐ 并查集的 Find 和 Union 操作原理(常结合 Kruskal 考察)
  • ⭐ 时间复杂度 O(ElogE) 的推导及瓶颈分析
  • 最小生成树的性质:n 个顶点恰好 n-1 条边、权值之和最小、不唯一(当存在等权边时)
  • 判断给定图是否存在最小生成树(图必须连通)

易错:判断"加入这条边是否形成回路"需要用并查集(Union-Find)。如果两个端点已经在同一个集合中,加入这条边就会形成回路。408 不一定要求写并查集代码,但要理解这个判断逻辑。

易错:Kruskal 适合稀疏图(边少),Prim 适合稠密图(顶点多但边密集)。原因是 Kruskal 的时间复杂度主要取决于边数 O(E log E),Prim 朴素实现取决于顶点数 O(V^2)。

相关知识

  • Prim 算法:MST 的另一种经典算法,适合稠密图
  • 邻接表:稀疏图的高效存储方式,Kruskal 常配合边集数组使用
  • 排序:Kruskal 的第一步是对所有边排序,排序效率直接影响整体性能

真题练习

相关真题(5题)

2023Q6选择题2分

图的最短路径:权值均为 1 的图中 BFS 可求单源最短路径,Prim/Kruskal 不行

2020Q7选择题2分

Kruskal 算法:按权值递增选边构造最小生成树的过程

2018Q42综合题15分

最小生成树应用:光通信网络最小成本规划(Prim/Kruskal)

2015Q6选择题2分

最小生成树:Kruskal 与 Prim 算法选边规则的差异

2012Q8选择题2分

最小生成树性质:MST 边权特性的判断