Skip to content

Kruskal 算法

为什么需要Kruskal 算法

最小生成树(MST)是图论中的经典问题:给定一个带权无向连通图,找到一棵包含所有顶点的树,使得边权之和最小。

Kruskal 算法是求解最小生成树的两大经典算法之一。它从的角度出发,按权值从小到大贪心选边,特别适合处理边稀疏的图。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 条边、权值之和最小、不唯一(当存在等权边时)
  • 判断给定图是否存在最小生成树(图必须连通)