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

Prim算法剖析与py/cpp/java语言实现

Prim算法剖析与py/cpp/Java语言实现

    • 一、Prim 算法是什么
    • 二、Prim 算法的原理
      • (一)基本概念
      • (二)核心思想
      • (三)算法步骤详解
    • 三、Prim 算法的代码实现
      • (一)Python 实现
      • (二)C++ 实现
      • (三)Java 实现
    • 四、案例分析与应用场景
      • (一)实际案例演示
      • (二)应用场景
    • 总结与思考
      • (一)Prim 算法总结
      • (二)拓展思考

一、Prim 算法是什么

图论中最小生成树(Minimum Spanning Tree, MST)问题是一个经典且基础的问题,给定一个连通无向图,图中的每条边都有一个权重,最小生成树就是包含图中所有顶点,并且边的权重之和最小的一棵树。

Prim 算法是一种用来求解最小生成树的高效算法 ,于 1930 年由捷克数学家沃伊捷赫・亚尔尼克(Vojtěch Jarník)发现,之后在 1957 年由美国计算机科学家罗伯特・普里姆(Robert C. Prim)独立发现,1959 年艾兹格・迪科斯彻再次发现该算法,因此它也被称为 DJP 算法、亚尔尼克算法或普里姆-亚尔尼克算法。该算法可在加权连通图里搜索最小生成树,意即由此算法搜索到的边子集所构成的树中,不但包括了连通图里的所有顶点,且其所有边的权值之和亦为最小。

二、Prim 算法的原理

(一)基本概念

在深入了解 Prim 算法之前,我们先来明确一些图论中的基本概念:

  • 连通图:在无向图中,如果任意两个顶点之间都存在路径相连,则称该图为连通图。简单来说,就是图中不存在孤立的顶点,从任意一个顶点出发,都能通过若干条边到达其他任意顶点。例如,一个城市交通网络可以看作是一个连通图,每个城市是顶点,城市之间的道路就是边,人们可以通过这些道路从一个城市到达另一个城市。

  • 权值:在带权图中,每条边都被赋予一个数值,这个数值就是权值。权值可以表示很多实际意义,比如在表示城市交通网络的图中,权值可以表示两个城市之间道路的长度、通行时间或建设成本等;在通信网络中,权值可以表示节点之间的通信延迟或带宽成本。

  • 最小生成树:对于一个连通的带权无向图,最小生成树是一棵包含图中所有顶点的树,并且这棵树的边权之和是所有生成树中最小的。例如,在构建通信网络时,我们希望用最少的成本连接所有的节点,这个最小成本的连接方案就是最小生成树 。

(二)核心思想

Prim 算法基于贪心思想,从任意一个起始顶点开始,逐步构建最小生成树。它的核心思路是:在每一步选择与当前已生成树(初始时只有一个起始顶点)相连的所有边中,权值最小的边所对应的顶点加入到已生成树中,直到所有顶点都被包含在生成树中。每一次选择都是当前状态下的最优选择,通过这种贪心策略,最终得到的就是最小生成树。

prim

(三)算法步骤详解

  1. 初始化
  • 选择一个起始顶点,将其标记为已访问。例如,我们选择顶点 A 作为起始点。

  • 初始化一个距离数组dist,用于记录每个顶点到已访问顶点集合(初始时只有起始顶点)的距离。将起始顶点的距离设为 0,其他顶点的距离设为无穷大(在程序中常用一个很大的数来表示)。比如,dist[A]=0dist[其他顶点]=无穷大

  • 初始化一个父节点数组parent,用于记录最小生成树中每个顶点的父节点,初始时所有顶点的父节点设为 -1,表示它们还没有父节点。

  1. 寻找最小边
  • 遍历所有未访问的顶点,找到距离已访问顶点集合最近的顶点(即dist值最小的顶点)。假设当前找到的最小距离顶点是 B,它到已访问顶点集合的距离为dist[B]

  • 这条连接 B 与已访问顶点集合中某个顶点(设为 A)的边就是当前的最小边。

  1. 更新距离数组
  • 将找到的最小距离顶点 B 标记为已访问,表示它已加入到最小生成树中。

  • 遍历顶点 B 的所有邻接顶点 C,如果顶点 C 未被访问,并且边 (B, C) 的权值小于dist[C](即通过顶点 B 到达顶点 C 的距离比之前记录的距离更短),则更新dist[C]为边 (B, C) 的权值,并将parent[C]设为 B,表示顶点 C 在最小生成树中的父节点是 B。

  1. 重复步骤
  • 不断重复上述寻找最小边和更新距离数组的步骤,直到所有顶点都被访问。此时,通过parent数组可以构建出最小生成树。例如,通过parent数组可以找到每个顶点在最小生成树中的连接关系,从而绘制出最小生成树的结构。

三、Prim 算法的代码实现

(一)Python 实现

import heapqdef prim(graph):# 初始化最小生成树和总权重mst = []total_weight = 0# 从第一个节点开始start_node = list(graph.keys())[0]# 使用优先队列存储边edges = [(0, start_node, start_node)]visited = set()while edges:weight, u, v = heapq.heappop(edges)if v in visited:continuevisited.add(v)mst.append((u, v, weight))total_weight += weightfor next_node, next_weight in graph[v].items():if next_node not in visited:heapq.heappush(edges, (next_weight, v, next_node))return mst, total_weight# 示例图表示为邻接表
graph = {'A': {'B': 1, 'C': 4},'B': {'A': 1, 'C': 2, 'D': 5},'C': {'A': 4, 'B': 2, 'D': 1},'D': {'B': 5, 'C': 1}
}mst, total_weight = prim(graph)
print("最小生成树:", mst)
print("总权重:", total_weight)
  1. 定义图的数据结构:在 Python 中,我们使用字典来表示图。字典的键是顶点,值是另一个字典,其中键是邻接顶点,值是边的权重。例如,graph = {'A': {'B': 1, 'C': 4}, 'B': {'A': 1, 'C': 2, 'D': 5}, 'C': {'A': 4, 'B': 2, 'D': 1}, 'D': {'B': 5, 'C': 1}}表示一个图,其中顶点A与顶点B的边权重为 1,与顶点C的边权重为 4,以此类推。

  2. 实现 Prim 算法的函数

  • 初始化部分

    • mst列表用于存储最小生成树的边,初始化为空。

    • total_weight用于记录最小生成树的总权重,初始化为 0。

    • start_node选择图中的第一个顶点作为起始点。

    • edges使用优先队列(heapq实现)来存储边,初始时将起始点到自身的边(权重为 0)加入队列。

    • visited集合用于记录已经访问过的顶点,初始为空。

  • 主循环部分

    • 从优先队列edges中取出权重最小的边(weight, u, v)

    • 如果顶点v已经被访问过,跳过这条边,因为加入这条边会形成环。

    • 将顶点v标记为已访问,并将边(u, v)加入最小生成树mst,同时更新总权重total_weight

    • 遍历顶点v的所有邻接顶点next_node及其对应的权重next_weight,如果next_node未被访问过,则将边(next_weight, v, next_node)加入优先队列edges

  • 返回结果:最后返回最小生成树的边列表mst和总权重total_weight

(二)C++ 实现

#include <iostream>
#include <vector>
#include <queue>
#include <limits>using namespace std;// Prim算法求最小生成树
void prim(vector<vector<int>>& graph, int start) {int V = graph.size();vector<bool> inMST(V, false);vector<int> key(V, numeric_limits<int>::max());vector<int> parent(V, -1);key[start] = 0;priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;pq.push({0, start});while (!pq.empty()) {int u = pq.top().second;pq.pop();inMST[u] = true;for (int v = 0; v < V; ++v) {if (graph[u][v] != 0 &&!inMST[v] && graph[u][v] < key[v]) {key[v] = graph[u][v];parent[v] = u;pq.push({key[v], v});}}}// 打印最小生成树for (int i = 1; i < V; ++i) {cout << parent[i] << " -- " << i << " == " << graph[i][parent[i]] << endl;}
}
  1. 头文件与数据结构定义
  • 引入必要的头文件#include <iostream>用于输入输出,#include <vector>用于存储图的邻接矩阵和其他辅助数组,#include <queue>用于实现优先队列,#include <limits>用于获取整数的最大值。

  • 使用二维向量vector<vector<int>>来表示图的邻接矩阵,其中graph[i][j]表示顶点i和顶点j之间的边权重,如果graph[i][j] == 0表示这两个顶点之间没有直接边。

  1. Prim 算法函数
  • 初始化部分

    • V记录图中顶点的数量。

    • inMST向量用于标记每个顶点是否已经在最小生成树中,初始时所有顶点都不在最小生成树中。

    • key向量用于记录每个顶点到最小生成树的当前最小距离,初始时将所有顶点的距离设为无穷大(numeric_limits<int>::max()),将起始顶点的距离设为 0。

    • parent向量用于记录最小生成树中每个顶点的父节点,初始时所有顶点的父节点设为 -1。

    • 使用优先队列pq来存储顶点及其到最小生成树的当前最小距离,优先队列按照距离从小到大排序。

  • 主循环部分

    • 从优先队列pq中取出距离最小的顶点u

    • 将顶点u标记为已在最小生成树中。

    • 遍历顶点u的所有邻接顶点v,如果v不在最小生成树中,并且从uv的边权重小于v当前到最小生成树的距离key[v],则更新key[v]为从uv的边权重,将v的父节点设为u,并将v及其距离加入优先队列pq

  • 打印结果部分:最后遍历parent向量,打印最小生成树的边,格式为parent[i] -- i == graph[i][parent[i]],表示从父节点parent[i]到顶点i的边及其权重。

(三)Java 实现

import java.util.*;class Graph {private int V;private List<List<Node>> adj;Graph(int v) {V = v;adj = new ArrayList<>(v);for (int i = 0; i < v; ++i)adj.add(new ArrayList<>());}static class Node {int dest;int weight;Node(int dest, int weight) {this.dest = dest;this.weight = weight;}}void addEdge(int src, int dest, int weight) {adj.get(src).add(new Node(dest, weight));adj.get(dest).add(new Node(src, weight));}void primMST() {int[] parent = new int[V];int[] key = new int[V];boolean[] mstSet = new boolean[V];Arrays.fill(key, Integer.MAX_VALUE);PriorityQueue<Node> pq = new PriorityQueue<>(Comparator.comparingInt(n -> n.weight));key[0] = 0;pq.add(new Node(0, 0));while (!pq.isEmpty()) {Node node = pq.poll();int u = node.dest;mstSet[u] = true;for (Node neighbor : adj.get(u)) {int v = neighbor.dest;int weight = neighbor.weight;if (!mstSet[v] && weight < key[v]) {key[v] = weight;parent[v] = u;pq.add(new Node(v, weight));}}}printMST(parent);}void printMST(int[] parent) {System.out.println("Edge \tWeight");for (int i = 1; i < V; ++i)System.out.println(parent[i] + " -- " + i + " \t" + adj.get(i).get(findIndex(adj.get(i), parent[i])).weight);}private int findIndex(List<Node> list, int dest) {for (int i = 0; i < list.size(); i++) {if (list.get(i).dest == dest) {return i;}}return -1;}
  1. 定义图和相关类
  • 创建Graph类来表示图,其中包含顶点数量V和邻接表adj。邻接表adj是一个List<List<Node>>,其中每个内部列表存储与某个顶点相邻的节点及其边权重。

  • 定义Node类来表示图中的节点,包含目标顶点dest和边权重weight

  • Graph类的构造函数中初始化邻接表。

  • 提供addEdge方法用于向图中添加边,因为是无向图,所以在两个顶点的邻接表中都添加相应的节点。

  1. 实现 Prim 算法的方法
  • 初始化部分

    • parent数组用于记录最小生成树中每个顶点的父节点,初始时所有元素为 -1。

    • key数组用于记录每个顶点到最小生成树的当前最小距离,初始时所有元素设为Integer.MAX_VALUE,将起始顶点(这里设为 0)的距离设为 0。

    • mstSet数组用于标记每个顶点是否已经在最小生成树中,初始时所有顶点都不在最小生成树中。

    • 使用优先队列pq来存储节点及其到最小生成树的当前最小距离,优先队列按照距离从小到大排序。

  • 主循环部分

    • 从优先队列pq中取出距离最小的节点node,获取其对应的顶点u

    • 将顶点u标记为已在最小生成树中。

    • 遍历顶点u的所有邻接节点neighbor,获取邻接顶点v和边权重weight,如果v不在最小生成树中,并且从uv的边权重小于v当前到最小生成树的距离key[v],则更新key[v]为从uv的边权重,将v的父节点设为u,并将v及其距离加入优先队列pq

  • 打印结果部分:最后调用printMST方法,遍历parent数组,打印最小生成树的边及其权重。printMST方法中通过findIndex方法找到边的权重。

四、案例分析与应用场景

(一)实际案例演示

假设有如下带权无向图,顶点为 A、B、C、D、E,边及其权值如下:

  • A - B: 2

  • A - C: 4

  • B - C: 1

  • B - D: 7

  • C - D: 3

  • C - E: 5

  • D - E: 2

  1. 初始化
  • 选择顶点 A 作为起始点,标记 A 为已访问。

  • dist[A]=0dist[B]=无穷大dist[C]=无穷大dist[D]=无穷大dist[E]=无穷大

  • parent[A]= -1parent[B]= -1parent[C]= -1parent[D]= -1parent[E]= -1

  1. 第一次寻找最小边
  • 遍历与 A 相连的边,A - B 权值为 2,A - C 权值为 4。

  • 最小边为 A - B,将 B 标记为已访问。

  • 更新dist[C]=1(因为 B - C 权值为 1,比之前dist[C]的无穷小),parent[C]=Bdist[D]=7(B - D 权值为 7),parent[D]=B

  1. 第二次寻找最小边
  • 考虑已访问顶点 A 和 B,与它们相连的未访问顶点的边有 B - C(权值 1),B - D(权值 7),A - C(权值 4)。

  • 最小边为 B - C,将 C 标记为已访问。

  • 更新dist[D]=3(因为 C - D 权值为 3,比之前dist[D]的 7 小),parent[D]=Cdist[E]=5(C - E 权值为 5),parent[E]=C

  1. 第三次寻找最小边
  • 考虑已访问顶点 A、B、C,与它们相连的未访问顶点的边有 C - D(权值 3),C - E(权值 5),B - D(权值 7)。

  • 最小边为 C - D,将 D 标记为已访问。

  • 更新dist[E]=2(因为 D - E 权值为 2,比之前dist[E]的 5 小),parent[E]=D

  1. 第四次寻找最小边
  • 考虑已访问顶点 A、B、C、D,与它们相连的未访问顶点的边只有 D - E(权值 2)。

  • 最小边为 D - E,将 E 标记为已访问。

  1. 构建最小生成树
  • 此时所有顶点都已访问,通过parent数组可以构建最小生成树。

  • 最小生成树的边为 A - B,B - C,C - D,D - E,总权重为 2 + 1 + 3 + 2 = 8。

(二)应用场景

  1. 通信网络建设:在构建通信网络时,每个基站可以看作是图的顶点,基站之间的连接线路看作是边,线路建设成本看作是边的权值。使用 Prim 算法可以找到最小成本的连接方案,确保所有基站都能连通,同时降低建设成本。例如,在一个城市中部署 5G 基站,需要连接各个基站形成一个通信网络。通过 Prim 算法,可以选择最优的连接方式,减少不必要的线路铺设,从而节省大量的建设资金和维护成本 。

  2. 电力传输线路规划:电力传输网络中,发电站、变电站和用电区域可以视为顶点,输电线路为边,建设和维护输电线路的成本为权值。Prim 算法能够帮助规划出成本最低的输电线路布局,确保电力能够高效传输到各个区域,同时减少线路损耗和建设投资。例如,在一个地区规划新的电网,利用 Prim 算法可以确定最优的输电线路走向,避免冗余建设,提高电力传输的经济性和可靠性。

  3. 交通路线设计:在城市交通规划或物流配送路线设计中,城市中的各个区域或配送点是顶点,道路或运输路线是边,建设道路的成本、运输时间或运输成本等可以作为权值。Prim 算法可以用于设计最小成本或最短时间的交通路线或配送路线,提高交通效率和物流配送效率。比如,一家快递公司规划配送路线,要将包裹从仓库送到多个配送点,使用 Prim 算法可以找到总运输距离最短或运输时间最短的路线,降低运输成本,提高配送效率。

总结与思考

(一)Prim 算法总结

Prim 算法作为求解最小生成树的经典算法,基于贪心思想,从一个起始顶点开始,不断选择与已生成树相连的最小权值边,逐步构建包含所有顶点的最小生成树。其实现过程中,通过距离数组记录顶点到已生成树的距离,利用优先队列等数据结构高效地寻找最小权值边 。

在实际应用中,Prim 算法广泛应用于通信网络建设、电力传输线路规划、交通路线设计等领域,帮助解决资源优化配置、成本最小化等实际问题。通过案例分析,我们直观地看到了 Prim 算法在具体场景中的应用步骤和效果。

(二)拓展思考

  1. 与其他最小生成树算法的比较:与 Prim 算法同为最小生成树算法的 Kruskal 算法,从边的角度出发,先对所有边按权值从小到大排序,然后依次选择权值最小且不会形成环的边加入最小生成树。而 Prim 算法从顶点角度出发,以一个顶点为起点逐步扩展生成树。Kruskal 算法更适用于稀疏图,因为它主要操作边,不需要维护复杂的顶点距离信息;而 Prim 算法在稠密图中表现较好,因为它通过优先队列等方式在每次选择边时可以快速找到最小权值边,减少了不必要的边的比较 。

  2. 优化方向和改进
    数据结构优化:在实现 Prim 算法时,使用更高效的数据结构可以显著提高算法性能。例如,使用斐波那契堆代替普通二叉堆作为优先队列,斐波那契堆在删除最小元素和降低键值操作上具有更低的时间复杂度,能将 Prim 算法的时间复杂度从 O ( E log ⁡ V ) O(E\log V) O(ElogV)降低到 O ( E + V log ⁡ V ) O(E + V\log V) O(E+VlogV),在处理大规模图时优势明显 。
    并行化处理:随着多核处理器的普及,将 Prim 算法并行化是一个重要的优化方向。可以将图划分为多个子图,在不同的处理器核心上同时执行 Prim 算法的部分步骤,最后合并结果。例如,在分布式系统中,将图的顶点和边分配到不同的节点上进行计算,通过消息传递机制同步信息,实现并行计算,从而提高算法的执行效率,加快最小生成树的求解速度 。

http://www.xdnf.cn/news/671059.html

相关文章:

  • 在 Linux 系统上连接 GitHub 的方法 (适用2025年)
  • idea配置android--以idea2023为例
  • 无锁编程介绍
  • 卫星姿态描述基础知识学习记录(部分)
  • MCP如何助力环境保护?——数据智能与Python的绿色革命
  • C++(初阶)(二十)——封装实现set和map
  • Python打卡训练营学习记录Day38
  • 25、web场景-【源码分析】-静态资源原理
  • Mongodb | 基于Springboot开发综合社交网络应用的项目案例(中英)
  • VS Code 安装后设置中文界面并添加常用插件的详细指南
  • 仿盒马》app开发技术分享-- 确认订单页(数据展示)(端云一体)
  • 过河卒--记忆化搜索
  • OpenHarmony平台驱动使用(五),HDMI
  • Python实现VTK-自学笔记(5):在三维世界里自由舞蹈——高级交互与动态可视化
  • @recogito/annotorious图像标注库
  • java 项目登录请求业务解耦模块全面
  • (自用)Java学习-5.16(取消收藏,批量操作,修改密码,用户更新,上传头像)
  • 基于 Operator 部署 Prometheus 实现 K8S 监控
  • Spark实时流数据处理实例(SparkStreaming通话记录消息处理)
  • 【md2html python 将 Markdown 文本转换为 HTML】
  • HTML Day02
  • pythonday30
  • Spark SQL进阶:解锁大数据处理的新姿势
  • AG32 DMAC实现内部MCU与FPGA通信【知识库】
  • 运维自动化工具 ansible 知识点总结
  • 域控账号密码抓取
  • C++数据结构 : 哈希表的实现
  • 2025上半年软考高级系统架构设计师经验分享
  • 第十一节:第一部分:正则表达式:应用案例、爬取信息、搜索替换
  • 牙科低对比度模体,衡量牙科影像设备的性能和诊断能力的工具