NeFut Logo NeFut
EN 管理员登录

深入解析最小生成树算法:Kruskal与Prim的对比与实现

发布于:2026-05-27 09:17 最后更新:2026-06-06 13:04
#algorithm #Graph #MST

核心逻辑与数学原理

最小生成树(Minimum Spanning Tree, MST)问题的本质是:在一个包含 $N$ 个点、$M$ 条边的带权无向连通图 $G=(V, E)$ 中,选择一个边集 $T \subseteq E$,使得 $(V, T)$ 构成一棵拓扑连通树,且该树的所有边权之和 $\sum_{e \in T} w(e)$ 达到全局最小值。

在 NOIP 体系下,Kruskal 与 Prim 算法分别基于截然不同的图论代数原理构建:

1. 边集贪心范式:Kruskal 算法

Kruskal 算法的核心数学基石是管辖割边定理(Cut Property)贪心扩张

2. 点集拓扑扩张范式:Prim 算法

Prim 算法的核心物理本质是蓝白点集合的切分扩张,其结构上与 Dijkstra 算法高度同构。


算法推导与状态设计

1. Kruskal 状态设计

定义并查集数组 fa[i]

  1. 边结构体封装:
struct Edge {
    int u, v;
    long long w;
    bool operator<(const Edge& rhs) const { return w < rhs.w; }
};
  1. 状态转移(并查集判环): 对于排好序的边 $e_i$,令 $root_u = \text{find}(e_i.u), \, root_v = \text{find}(e_i.v)$。

$$\text{if } (root_u \neq root_v) \quad \text{fa}[root_u] = root_v, \,\, \text{edge\_cnt} \leftarrow \text{edge\_cnt} + 1$$

2. Prim 堆优化状态设计

定义 $dist[i]$ 为白点 $i$ 到当前已生成的 MST 边集树面的拓扑最短距离。
优先队列中存储的状态为二元组 $(d, u)$,其中 $u$ 为节点编号,$d$ 为当前的 $dist[u]$。
小根堆顶元素驱动状态转移方程:

$$dist[v] = \min(dist[v], w(u, v)) \quad (\forall v \in T, \, (u, v) \in E)$$

物理含义:当蓝点集扩展了 $u$ 后,悬空点 $v$ 到树面的最短距离,可能被新加入的边界边 $w(u, v)$ 刷新。


C++ 标准源码(Kruskal 工业级模板)

#include <iostream>
#include <vector>
#include <algorithm>

const int MAXN = 100005;
const int MAXM = 300005;

struct Edge {
    int u, v;
    long long w;
    // 严格按边权非降序排列
    bool operator<(const Edge& rhs) const {
        return w < rhs.w;
    }
};

Edge edges[MAXM];
int fa[MAXN];

// 并查集核心:路径压缩
int find_set(int i) {
    if (fa[i] == i) return i;
    return fa[i] = find_set(fa[i]); // 关键踩坑点:必须进行路径压缩,否则链状树会导致复杂度退化
}

// 并查集内连通块合并
bool union_set(int i, int j) {
    int root_i = find_set(i);
    int root_j = find_set(j);
    if (root_i != root_j) {
        fa[root_i] = root_j;
        return true; // 成功合并,说明原本不连通,此边合法
    }
    return false; // 原本已连通,若加边必成环,拒绝
}

int main() {
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);

    int n, m;
    if (!(std::cin >> n >> m)) return 0;

    for (int i = 0; i < m; ++i) {
        std::cin >> edges[i].u >> edges[i].v >> edges[i].w;
    }

    // 核心驱动:对全量边集进行快速排序
    std::sort(edges, edges + m);

    // 初始化并查集底座
    for (int i = 1; i <= n; ++i) fa[i] = i;

    long long mst_weight = 0;
    int edges_selected = 0;

    for (int i = 0; i < m; ++i) {
        if (union_set(edges[i].u, edges[i].v)) {
            mst_weight += edges[i].w;
            edges_selected++;
            if (edges_selected == n - 1) break; // 提前熟化拦截:已凑齐 N-1 条边
        }
    }

    // 拓扑连通性校验
    if (edges_selected < n - 1) {
        std::cout << "orz\n"; // 图本身不连通,无法构造生成树
    } else {
        std::cout << mst_weight << "\n";
    }

    return 0;
}

NOIP 实战避坑指南

  1. 完全不看数据特征,稠密网格图盲目选用 Kruskal: 若题目给出的图是严格的稠密图,例如完全图或大基数网格图,满足 $V=5000, M \approx 1.2 \times 10^7$。若选用 Kruskal 算法,其排序开销 $M \log M$ 将让内存膨胀且 CPU 时间直接暴崩触发 TLE。此时,必须敏锐切入 $O(V^2)$ 的朴素 Prim 或 堆优化 Prim 算法,因为 Prim 的松弛状态完全不依赖于边数的全局排序。
  2. 并查集初始化漏写或深度递归未路径压缩: 在 Kruskal 的 main 函数中,极其容易遗漏 for (int i = 1; i <= n; ++i) fa[i] = i; 这句标定。若不初始化,所有的 fa 残留为 0,find_set 将瞬间陷入死循环或内存越界。此外,若在 find_set 中漏掉滚动赋值 fa[i] = find_set(fa[i])(路径压缩),并查集退化为长链,单次合并退化为 $O(V)$,导致 Kruskal 算法退化为 $O(MV)$ 的暴力,彻底丢失时效性。

经典 NOIP/洛谷 真题

1. 洛谷 P3366 【模板】最小生成树

2. 洛谷 P1194 买礼物

原文链接: local://11.1

[h] 返回首页