18.Kruskal 算法:用最短的边连成一张网

一、什么是 Kruskal 算法?

Kruskal 算法是一种最小生成树算法 ,由 Joseph Kruskal 在 1956 年提出。它的核心思想是每次选择最短的边,将两个连通分量合并,直到所有节点都连通,最终得到一棵总权重最小的生成树。

简单来说,Kruskal 算法就像 "用最少的钱连接所有城市":

  • 你有 23 座城市,要修公路把它们全部连通;
  • 每次先修最短的公路,连接两个还没连通的城市;
  • 跳过那些会形成环路的公路,直到所有城市都连通;
  • 这样修出来的公路总长度最短,也就是最小生成树。

二、最小生成树的核心概念

最小生成树(Minimum Spanning Tree,简称 MST)是指在一个连通的带权无向图中,选择 n-1 条边,将所有 n 个节点连通,且总权重最小的子图。

最小生成树的特点:

  • 包含所有节点;
  • 没有环路;
  • 总权重最小;
  • 边数为 n-1。

三、Kruskal 算法的核心步骤

Kruskal 算法的核心步骤可以分为以下几步:

  1. 排序:将所有边按权重从小到大排序;
  2. 初始化:每个节点自己是一个连通分量;
  3. 选择边:依次选择最短的边,判断两个节点是否在同一个连通分量;
  4. 合并:如果不在同一个连通分量,合并两个连通分量,将边加入最小生成树;
  5. 重复:直到所有节点都连通。

四、Kruskal 算法的关键:并查集

Kruskal 算法的核心是并查集(Union-Find),它用于快速判断两个节点是否在同一个连通分量,以及合并两个连通分量。

并查集的核心操作:

  • find:查找节点的根节点;
  • union:合并两个连通分量。

五、Kruskal 算法的代码实现

1. Python 版本(直观易懂)

复制代码
class UnionFind:
    def __init__(self, size):
        self.parent = list(range(size))
    
    def find(self, x):
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])  # 路径压缩
        return self.parent[x]
    
    def union(self, x, y):
        x_root = self.find(x)
        y_root = self.find(y)
        if x_root != y_root:
            self.parent[y_root] = x_root
            return True
        return False

def kruskal(edges, num_nodes):
    # 按权重从小到大排序
    edges.sort(key=lambda x: x[2])
    uf = UnionFind(num_nodes)
    mst = []
    total_weight = 0
    
    for u, v, weight in edges:
        if uf.union(u, v):
            mst.append((u, v, weight))
            total_weight += weight
            # 已经连通所有节点
            if len(mst) == num_nodes - 1:
                break
    
    return mst, total_weight

# 测试:城市连接问题
# 城市编号:0=北京,1=石家庄,2=太原,3=广州,4=深圳,5=重庆,6=贵阳,7=郑州,8=合肥,9=济南,10=台北
edges = [
    (0, 1, 89),   # 北京-石家庄
    (1, 2, 60),   # 石家庄-太原
    (3, 4, 32),   # 广州-深圳
    (5, 6, 106),  # 重庆-贵阳
    (7, 8, 153),  # 郑州-合肥
    (8, 9, 175),  # 合肥-济南
    (0, 2, 138),  # 北京-太原(已连通,跳过)
    (9, 1, 104),  # 济南-石家庄
    (10, 4, 243), # 台北-深圳
    (0, 11, 223), # 北京-沈阳
]

mst, total_weight = kruskal(edges, 12)
print("最小生成树边:", mst)
print("总权重:", total_weight)

2. C 语言版本(更贴近底层)

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAX_EDGES 100
#define MAX_NODES 23

// 边结构体
typedef struct {
    int u, v, weight;
} Edge;

// 并查集结构体
typedef struct {
    int parent[MAX_NODES];
} UnionFind;

// 初始化并查集
void initUnionFind(UnionFind* uf, int size) {
    for (int i = 0; i < size; i++) {
        uf->parent[i] = i;
    }
}

// 查找根节点(路径压缩)
int find(UnionFind* uf, int x) {
    if (uf->parent[x] != x) {
        uf->parent[x] = find(uf, uf->parent[x]);
    }
    return uf->parent[x];
}

// 合并两个连通分量
int unionSets(UnionFind* uf, int x, int y) {
    int x_root = find(uf, x);
    int y_root = find(uf, y);
    if (x_root != y_root) {
        uf->parent[y_root] = x_root;
        return 1;
    }
    return 0;
}

// 比较函数,用于排序边
int compareEdges(const void* a, const void* b) {
    return ((Edge*)a)->weight - ((Edge*)b)->weight;
}

// Kruskal算法
void kruskal(Edge edges[], int num_edges, int num_nodes) {
    // 按权重从小到大排序
    qsort(edges, num_edges, sizeof(Edge), compareEdges);
    
    UnionFind uf;
    initUnionFind(&uf, num_nodes);
    
    Edge mst[MAX_NODES];
    int mst_size = 0;
    int total_weight = 0;
    
    for (int i = 0; i < num_edges; i++) {
        int u = edges[i].u;
        int v = edges[i].v;
        int weight = edges[i].weight;
        
        if (unionSets(&uf, u, v)) {
            mst[mst_size++] = edges[i];
            total_weight += weight;
            // 已经连通所有节点
            if (mst_size == num_nodes - 1) {
                break;
            }
        }
    }
    
    // 打印结果
    printf("最小生成树边:\n");
    for (int i = 0; i < mst_size; i++) {
        printf("%d-%d,权重:%d\n", mst[i].u, mst[i].v, mst[i].weight);
    }
    printf("总权重:%d\n", total_weight);
}

int main() {
    // 城市连接问题
    Edge edges[] = {
        {0, 1, 89},   // 北京-石家庄
        {1, 2, 60},   // 石家庄-太原
        {3, 4, 32},   // 广州-深圳
        {5, 6, 106},  // 重庆-贵阳
        {7, 8, 153},  // 郑州-合肥
        {8, 9, 175},  // 合肥-济南
        {0, 2, 138},  // 北京-太原(已连通,跳过)
        {9, 1, 104},  // 济南-石家庄
        {10, 4, 243}, // 台北-深圳
        {0, 11, 223}, // 北京-沈阳
    };
    int num_edges = sizeof(edges) / sizeof(edges[0]);
    int num_nodes = 12;
    
    kruskal(edges, num_edges, num_nodes);
    return 0;
}

六、Kruskal 算法的特点

  • 时间复杂度:O (m log m),其中 m 是边数,主要由排序操作决定;
  • 空间复杂度:O (n),需要存储并查集;
  • 适用场景:边数较少的稀疏图;
  • 是否含环:不会形成环,因为每次只合并两个不同的连通分量。

七、Kruskal 算法的优化

为了提高 Kruskal 算法的效率,可以进行以下优化:

  1. 路径压缩:在并查集的 find 操作中进行路径压缩,减少查找时间;
  2. 按秩合并:在并查集的 union 操作中按秩合并,减少树的高度;
  3. 快速排序:使用快速排序代替其他排序算法,提高排序效率。

八、Kruskal 算法的实际应用场景

Kruskal 算法是一种非常基础且重要的算法,常见场景包括:

  1. 网络设计:设计公路网、电网、通信网,最小化建设成本;
  2. 集群计算:将多个节点连接成一个集群,最小化通信成本;
  3. 图像处理:图像分割,将相似的像素合并成一个区域;
  4. 社交网络:找到最小的社交连接,将所有用户连通。

九、Kruskal 算法 vs Prim 算法

Kruskal 算法和 Prim 算法是两种最常用的最小生成树算法,它们的区别如下:

十、总结

Kruskal 算法是一种最小生成树算法,它通过每次选择最短的边,合并两个连通分量,直到所有节点都连通,最终得到一棵总权重最小的生成树。

Kruskal 算法的核心是并查集,它可以快速判断两个节点是否在同一个连通分量,以及合并两个连通分量。

在实际应用中,Kruskal 算法广泛应用于网络设计、集群计算、图像处理等领域,是一种非常基础且重要的算法。

希望这篇文章能帮助你理解 Kruskal 算法的原理和实现!

相关推荐
Evand J1 小时前
【MATLAB例程,图像降噪7】高斯加权滑动窗口滤波,用于图像降噪,带质量评价,附代码下载链接
开发语言·图像处理·matlab·滑动窗口·滤波·降噪·图像滤波
十年一梦惊觉醒1 小时前
快手视频发布助手,全自动视频发布带货工具
开发语言·前端·javascript
Java后端的Ai之路1 小时前
一文搞懂 GitHub Actions-CICD
开发语言·大模型·github·cicd·action
码匠许师傅1 小时前
【C++三方组件】开篇总论:为什么需要以及如何选型?
开发语言·c++
cvby1 小时前
C++11--右值引用和移动语义
开发语言·c++
pen-ai1 小时前
【优化方法】最小二乘:从误差平方到线性与非线性拟合
人工智能·算法·机器学习
en.en..1 小时前
TCP/UDP 收发流程(网络字节序---函数讲解)
开发语言·php
名字还没想好☜2 小时前
Go 用 json.Decoder 流式解析大 JSON:边读边处理不爆内存
开发语言·后端·golang·go·json
xiangyun612 小时前
【408数据结构 03】线性表与顺序表:C++手写SeqList
开发语言·数据结构·c++