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

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

一、Prim 算法是什么

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

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

二、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 实现

python 复制代码
import heapq


def 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:
            continue
        visited.add(v)
        mst.append((u, v, weight))
        total_weight += weight

        for 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++ 实现

cpp 复制代码
#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 实现

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 算法的部分步骤,最后合并结果。例如,在分布式系统中,将图的顶点和边分配到不同的节点上进行计算,通过消息传递机制同步信息,实现并行计算,从而提高算法的执行效率,加快最小生成树的求解速度 。

相关推荐
苏荷水1 分钟前
day12 leetcode-hot100-20(矩阵3)
算法·leetcode·矩阵
全栈凯哥3 分钟前
Java详解LeetCode 热题 100(21):LeetCode 240. 搜索二维矩阵 II(Search a 2D Matrix II)详解
java·算法·leetcode
武子康12 分钟前
大数据-273 Spark MLib - 基础介绍 机器学习算法 决策树 分类原则 分类原理 基尼系数 熵
大数据·人工智能·算法·决策树·机器学习·spark-ml
肥猪猪爸1 小时前
使用LSTM进行时间序列分析
数据结构·人工智能·rnn·深度学习·算法·lstm·时间序列分析
哈听星2 小时前
数值积分实验
算法
Musennn3 小时前
leetcode106.从中序与后序遍历序列构造二叉树:索引定位与递归分治的完美配合
java·数据结构·算法·leetcode
OKkankan4 小时前
类和对象(中1)
c语言·数据结构·c++·算法
共享家95274 小时前
算法刷题记录:滑动窗口经典题目解析
c++·算法·leetcode
啥都想学的又啥都不会的研究生5 小时前
常规算法学习
java·数据结构·b树·学习·算法·排序算法
I AM_SUN6 小时前
153. 寻找旋转排序数组中的最小值
数据结构·c++·算法·leetcode·二分法