当前位置: 首页 > news >正文

深入浅出【最小生成树】:Prim与Kruskal算法详解

深入浅出最小生成树:Prim与Kruskal算法详解

什么是最小生成树 (Minimum Spanning Tree, MST)?

在图论中,我们经常会遇到这样一个经典问题:对于一个带权的无向连通图,如何找到一个无环的子集,使得这个子集连接了图中所有的顶点,并且所有边的权重之和最小

这个子集就构成了一棵最小生成树(MST)。它有两个关键属性:

  1. 是一棵树:意味着无环且连通。
  2. 权重最小:在所有可能的生成树中,它的总边权最小。

应用场景:最小生成树在现实生活中应用广泛,例如:

  • 网络布线:用最少的线缆连接所有机房或城市。
  • 交通规划:建设成本最低的公路网连接所有城镇。
  • 电路设计:用最少的线路连接多个元件。

两大经典算法

解决MST问题最著名的两个算法是Prim算法Kruskal算法。它们都基于贪心算法的思想,即在每一步选择中都采取当前状态下最优的选择。

1. Prim算法(普里姆算法)

原理解释

Prim算法的核心思想是:从某一个顶点开始,逐步“长大”一棵树。每次将一条连接树与非树节点最小权重边及其连接的顶点加入到树中。

你可以把它想象成“种树”的过程:

  1. 随便撒下一颗种子(选择一个起始顶点)。
  2. 找到离这棵树最近的一棵“树苗”(权重最小的边连接的顶点)。
  3. 把这棵“树苗”和连接它的那根“树枝”(边)并入大树。
  4. 重复步骤2和3,直到所有的“树苗”都被并入大树。
算法步骤
  1. 初始化:任选一个顶点作为起始点,加入集合S(代表已在MST中的顶点)。初始化一个数组dist(或key),记录所有不在S中的顶点到S最短距离(即与S中任意顶点相连的最小边权)。初始时,起始点的dist设为0,其他点为无穷大(INF)。
  2. 循环迭代:重复以下步骤V-1次(V为顶点数):
    a. 贪心选择:从不在S的顶点中,选出dist值最小的顶点u
    b. 加入集合:将u加入集合S。此时,连接uS的那条边(就是使得dist[u]最小的边)就是MST的一条边。
    c. 更新信息:检查所有与u相邻且不在S中的顶点v。如果边(u, v)的权重小于v当前的dist[v]值,则更新dist[v] = graph[u][v](同时可以记录parent[v] = u,表示v是通过u加入树的)。
  3. 结束:最终,parent数组和dist数组共同描述了整棵最小生成树。
代码实现

C++实现(使用邻接矩阵)

#include <iostream>
#include <vector>
#include <climits>
using namespace std;#define V 5 // 图中顶点的数量int minKey(const vector<int>& key, const vector<bool>& inMST) {int min = INT_MAX, min_index;for (int v = 0; v < V; v++) {if (!inMST[v] && key[v] < min) {min = key[v];min_index = v;}}return min_index;
}void printMST(const vector<int>& parent, const vector<vector<int>>& graph) {cout << "Edge \tWeight\n";for (int i = 1; i < V; i++) {cout << parent[i] << " - " << i << " \t" << graph[i][parent[i]] << " \n";}
}void primMST(const vector<vector<int>>& graph) {vector<int> parent(V);     // 存储MST的结构vector<int> key(V, INT_MAX); // 记录顶点到MST的最小权值vector<bool> inMST(V, false); // 记录顶点是否已在MST中key[0] = 0;     // 选择第0个顶点作为起点parent[0] = -1; // 第一个节点是树的根,没有父节点for (int count = 0; count < V - 1; count++) {int u = minKey(key, inMST); // 选取key最小的顶点inMST[u] = true;            // 将其加入MST// 更新所有与u相邻的顶点的key值和parentfor (int v = 0; v < V; v++) {if (graph[u][v] && !inMST[v] && graph[u][v] < key[v]) {parent[v] = u;key[v] = graph[u][v];}}}printMST(parent, graph);
}int main() {vector<vector<int>> graph = { { 0, 2, 0, 6, 0 },{ 2, 0, 3, 8, 5 },{ 0, 3, 0, 0, 7 },{ 6, 8, 0, 0, 9 },{ 0, 5, 7, 9, 0 } };primMST(graph);return 0;
}

Python实现

import sysclass Graph():def __init__(self, vertices):self.V = verticesself.graph = [[0 for _ in range(vertices)] for _ in range(vertices)]def print_mst(self, parent):print("Edge \tWeight")for i in range(1, self.V):print(f"{parent[i]} - {i} \t{self.graph[i][parent[i]]}")def min_key(self, key, mst_set):min_val = sys.maxsizemin_index = -1for v in range(self.V):if key[v] < min_val and not mst_set[v]:min_val = key[v]min_index = vreturn min_indexdef prim_mst(self):key = [sys.maxsize] * self.V  # 初始化key值为无穷大parent = [None] * self.V      # 存储MSTkey[0] = 0                    # 选择第一个顶点作为起点mst_set = [False] * self.V    # 记录顶点是否在MST中parent[0] = -1                # 根节点没有父节点for _ in range(self.V - 1):u = self.min_key(key, mst_set)mst_set[u] = Truefor v in range(self.V):if self.graph[u][v] > 0 and not mst_set[v] and key[v] > self.graph[u][v]:key[v] = self.graph[u][v]parent[v] = uself.print_mst(parent)if __name__ == '__main__':g = Graph(5)g.graph = [[0, 2, 0, 6, 0],[2, 0, 3, 8, 5],[0, 3, 0, 0, 7],[6, 8, 0, 0, 9],[0, 5, 7, 9, 0]]g.prim_mst()

2. Kruskal算法(克鲁斯卡尔算法)

原理解释

Kruskal算法的核心思想是:按权重从小到大选择边,如果这条边不会与已选择的边构成环,就将其加入MST

你可以把它想象成“拼图”的过程:

  1. 把所有边按权重从小到大排序。
  2. 初始化一个只有V个顶点,没有边的森林。
  3. 从权重最小的边开始尝试:
    • 如果这条边连接的两个顶点不在同一个连通分量中(即加入后不会形成环),就将这条边加入MST,并合并这两个连通分量。
    • 否则,丢弃这条边。
  4. 重复步骤3,直到MST中有V-1条边。

这里的关键是如何高效判断是否成环,这可以通过并查集 (Union-Find) 数据结构来高效实现。

算法步骤
  1. 排序:将图的所有边按权重从小到大排序。
  2. 初始化:初始化一个并查集,每个顶点自成一个集合。初始化MST为空。
  3. 处理边:按顺序遍历每一条边:
    a. 检查环:使用并查集检查这条边连接的两个顶点uv是否在同一个集合中。
    b. 若无环则加入:如果不在,说明加入这条边不会形成环,将其加入MST。然后在并查集中合并uv所在的集合。
  4. 结束:当MST中有V-1条边时,算法结束。
代码实现

C++实现(使用并查集)

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;class Edge {
public:int src, dest, weight;// 用于sort函数比较bool operator<(const Edge& other) const {return weight < other.weight;}
};class Subset {
public:int parent;int rank;
};class Graph {
public:int V, E;vector<Edge> edges;Graph(int v, int e) : V(v), E(e) {}int find(Subset subsets[], int i) {if (subsets[i].parent != i)subsets[i].parent = find(subsets, subsets[i].parent);return subsets[i].parent;}void Union(Subset subsets[], int x, int y) {int xroot = find(subsets, x);int yroot = find(subsets, y);if (subsets[xroot].rank < subsets[yroot].rank)subsets[xroot].parent = yroot;else if (subsets[xroot].rank > subsets[yroot].rank)subsets[yroot].parent = xroot;else {subsets[yroot].parent = xroot;subsets[xroot].rank++;}}void kruskalMST() {vector<Edge> result; // 存储MST的结果int e = 0; // 用于遍历排序后的边int i = 0; // 用于result的索引// 1. 排序所有边sort(edges.begin(), edges.end());// 分配内存并创建V个子集Subset* subsets = new Subset[V];for (int v = 0; v < V; v++) {subsets[v].parent = v;subsets[v].rank = 0;}// MST的边数应为 V-1while (i < V - 1 && e < E) {// 2. 选取最小的边Edge next_edge = edges[e++];int x = find(subsets, next_edge.src);int y = find(subsets, next_edge.dest);// 如果不在同一集合,加入不会成环if (x != y) {result.push_back(next_edge);Union(subsets, x, y);i++;}}// 输出MSTcout << "Edge \tWeight\n";for (i = 0; i < result.size(); i++) {cout << result[i].src << " - " << result[i].dest << " \t" << result[i].weight << endl;}delete[] subsets;}
};int main() {int V = 4;int E = 5;Graph graph(V, E);graph.edges = {{0, 1, 10},{0, 2, 6},{0, 3, 5},{1, 3, 15},{2, 3, 4}};graph.kruskalMST();return 0;
}

Python实现

class Graph:def __init__(self, vertices):self.V = verticesself.graph = [] # 存储所有边的列表def add_edge(self, u, v, w):self.graph.append([u, v, w])def find(self, parent, i):# 查找根节点if parent[i] != i:parent[i] = self.find(parent, parent[i]) # 路径压缩return parent[i]def union(self, parent, rank, x, y):# 按秩合并xroot = self.find(parent, x)yroot = self.find(parent, y)if rank[xroot] < rank[yroot]:parent[xroot] = yrootelif rank[xroot] > rank[yroot]:parent[yroot] = xrootelse:parent[yroot] = xrootrank[xroot] += 1def kruskal_mst(self):result = [] # 存储MST的边i = 0 # 遍历边的索引e = 0 # 结果中边的计数# 1. 按权重排序所有边self.graph = sorted(self.graph, key=lambda item: item[2])parent = []rank = []# 2. 初始化并查集for node in range(self.V):parent.append(node)rank.append(0)# 需要添加 V-1 条边while e < self.V - 1:if i >= len(self.graph):breaku, v, w = self.graph[i]i += 1x = self.find(parent, u)y = self.find(parent, v)# 如果不在同一集合,加入结果并合并if x != y:e += 1result.append([u, v, w])self.union(parent, rank, x, y)# 输出结果print("Edge \tWeight")for u, v, w in result:print(f"{u} -- {v} \t{w}")if __name__ == '__main__':g = Graph(4)g.add_edge(0, 1, 10)g.add_edge(0, 2, 6)g.add_edge(0, 3, 5)g.add_edge(1, 3, 15)g.add_edge(2, 3, 4)g.kruskal_mst()

算法对比与总结

特性Prim算法Kruskal算法
核心思想从一个点开始,逐步扩张树按权重排序边,逐个添加不构成环的边
数据结构优先队列(堆)、key数组并查集、排序后的边集合
时间复杂度O(V^2)(邻接矩阵)
O(E log V)(邻接表+二叉堆)
O(E log E)O(E log V)(主要来自排序)
适用场景稠密图(边多顶点少)稀疏图(边少顶点多)
贪心策略顶点贪心边贪心

如何选择?

  • 如果图的边数量非常庞大(稠密图),接近完全图,使用Prim算法(尤其是简单实现)可能更高效。
  • 如果图的边数量相对较少(稀疏图),Kruskal算法因其简单的排序和并查集操作,通常更容易实现且效率更高。

希望这篇博客能帮助你彻底理解最小生成树和这两大经典算法!

http://www.dtcms.com/a/346361.html

相关文章:

  • 111、【OS】【Nuttx】【周边】效果呈现方案解析:-print0 选项
  • AQS模板方法
  • 使用 Google 开源 AI 工具 LangExtract 进行结构化信息抽取
  • 单片机---------WIFI模块
  • Seaborn数据可视化实战:Seaborn数据可视化入门-绘制统计图表与数据分析
  • Dify 从入门到精通(第 49/100 篇):Dify 的自动化测试
  • STM32 硬件I2C读写MPU6050
  • 【链表 - LeetCode】24. 两两交换链表中的节点
  • 纯手撸一个RAG
  • 黄飞对话小熊电器流程与IT负责人:企业数字化进阶与AI实践如何落地?
  • QIcon::actualSize的作用和用法
  • 2025/8/22 xxl-job速通
  • 解决 微信开发者工具 :下载基础库版本 2.31.0 失败
  • RAG和微调是什么?两者的区别?什么场景使用RAG或微调?判断依据是什么?
  • LINUX网络编程--网络的发展与通信
  • AI赋能环保精准治理:AI水质监测溯源快、空气质量预测施策准,守护生态新效能
  • 关于 java+gradle的弹窗多选应用app
  • 【GPT入门】第54课 量化位数与存储大小的影响
  • Java 面试题训练助手 Web 版本
  • 网络通信——UDP协议。
  • Kubernetes 1.28 集群部署指南(基于 Containerd 容器运行时)
  • 笔记:二叉树构建方法
  • 从“配置化思维”到“前端效率革命”:xiangjsoncraft 如何用 JSON 简化页面开发?
  • 【源码】MES系统:从下达计划、执行反馈、异常预警到过程控制的一整套执行中枢。
  • FastTracker:实时准确的视觉跟踪
  • 一键部署openGauss6.0.2轻量版单节点
  • DPY-3010: connections to this database server version are not supported by p
  • LoRA内幕机制解析(53)
  • Design Compiler:层次模型(Block Abstraction)的简介
  • 什么是神鸟云?