菜单
×
   ❮   
HTML CSS JAVASCRIPT SQL PYTHON JAVA PHP HOW TO W3.CSS C C++ C# BOOTSTRAP REACT MYSQL JQUERY EXCEL XML DJANGO NUMPY PANDAS NODEJS R TYPESCRIPT ANGULAR GIT POSTGRESQL MONGODB ASP AI GO KOTLIN SASS VUE DSA GEN AI SCIPY AWS CYBERSECURITY DATA SCIENCE
     ❯   

DSA 普里姆算法

普里姆算法由捷克数学家 Vojtěch Jarník 于 1930 年发明。

该算法随后由 Robert C. Prim 于 1957 年重新发现,并由 Edsger W. Dijkstra 于 1959 年重新发现。因此,该算法有时也被称为“Jarník 算法”或“Prim-Jarník 算法”。

普里姆算法

普里姆算法在一个连通的无向图中寻找最小生成树 (MST)。


{{ msgDone }}

普里姆算法找到的 MST 是图中所有顶点连接在一起的边的集合,且边的权重总和最小。

普里姆算法通过首先将一个随机顶点添加到 MST 来找到 MST。然后,算法查找当前 MST 中权重最低的顶点,并将其添加到 MST。普里姆算法不断重复此过程,直到所有节点都包含在 MST 中。

普里姆算法是贪心算法,它有一种简单的方法来创建一个最小生成树。

要使普里姆算法正常工作,所有节点都必须是连通的。要查找非连通图的 MST,可以使用 Kruskal 算法。下一页将介绍 Kruskal 算法。

工作原理

  1. 选择一个随机顶点作为起点,并将其包含为 MST 中的第一个顶点。
  2. 比较从 MST 发出的边。选择连接 MST 顶点和 MST 外部顶点的权重最低的边。
  3. 将该边和顶点添加到 MST。
  4. 重复步骤 2 和 3,直到所有顶点都属于 MST。

注意:由于起始顶点是随机选择的,因此同一图可能包含不同的 MST 边,但 MST 的总边权重仍然具有相同的最小值。


手动演练

让我们手动运行普里姆算法,以便在尝试编程之前理解详细的步骤。

普里姆算法从随机顶点开始生长最小生成树 (MST),但在此演示中,顶点 A 被选为起始顶点。

{{ edge.weight }} {{el.name}}

从顶点 A 开始,MST 沿着权重最低的边生长。因此,顶点 A 和 D 现在属于属于最小生成树的顶点组。

{{ edge.weight }} {{el.name}}

一个 parents 数组是普里姆算法生长 MST 边方式的核心。

此时,parents 数组看起来像这样

parents = [-1,  0, -1,  0,  3,  3, -1, -1]
#vertices [ A,  B,  C,  D,  E,  F,  G,  H]

起始顶点 A 没有父节点,因此其值为 -1。D 的父节点是 A,这就是为什么 D 的父节点值为 0(顶点 A 位于索引 0)。B 的父节点也是 A,D 是 E 和 F 的父节点。

parents 数组帮助我们维护 MST 的树结构(一个顶点只能有一个父节点)。

此外,为了避免循环并跟踪当前在 MST 中的顶点,使用了 in_mst 数组。

此时,in_mst 数组看起来像这样

 in_mst = [ true, false, false,  true, false, false, false, false]
#vertices [    A,     B,     C,     D,     E,     F,     G,     H]

普里姆算法的下一步是包含一个新顶点作为 MST 的一部分,并选择最接近当前 MST 节点 A 和 D 的顶点。

由于 A-B 和 D-F 的权重都相同,最低边权重为 4,因此可以选择 B 或 F 作为下一个 MST 顶点。在此演示中,我们选择 B 作为下一个 MST 顶点。

{{ edge.weight }} {{el.name}}

如您所见,到 E 的 MST 边之前来自顶点 D,现在来自顶点 B,因为 B-E 的权重 6 低于 D-E 的权重 7。顶点 E 在 MST 树结构中(以及在 parents 数组中)只能有一个父节点,因此 B-E 和 D-E 不能同时成为到 E 的 MST 边。

MST 的下一个顶点是 C,因为 B-C 边的权重 3 是当前 MST 顶点之间的最短边权重。

{{ edge.weight }} {{el.name}}

随着顶点 C 被包含在 MST 中,会检查 C 的出边,以确定是否有权重更低的边从该 MST 顶点连接到 MST 外部的顶点。C-E 边的权重(3)低于之前的 B-E MST 边(6),并且 C-H 边以权重 2 被包含在 MST 中。

顶点 H 是下一个要包含在 MST 中的顶点,因为它具有最低的边权重 6,并且顶点 H 在 parents 数组中成为顶点 G 的父节点。

{{ edge.weight }} {{el.name}}

下一个要包含在 MST 中的顶点是 E 或 F,因为它们都有最低的边权重:4

在此演示中,我们选择顶点 E 作为下一个要包含在 MST 中的顶点。

{{ edge.weight }} {{el.name}}

下一个和最后两个要添加到 MST 的顶点是 F 和 G。D-F 是到 F 的 MST 边,E-G 是到 G 的 MST 边,因为这些边是当前 MST 的最低权重边。

运行下面的模拟,查看普里姆算法执行我们刚刚完成的手动步骤。

{{ edge.weight }} {{el.name}}
{{ msgDone }}

普里姆算法实现

为了让普里姆算法找到最小生成树 (MST),我们创建一个 Graph 类。稍后我们将使用此 Graph 类中的方法来创建上面示例中的图,并在其上运行普里姆算法。

class Graph:
    def __init__(self, size):
        self.adj_matrix = [[0] * size for _ in range(size)]
        self.size = size
        self.vertex_data = [''] * size

    def add_edge(self, u, v, weight):
        if 0 <= u < self.size and 0 <= v < self.size:
            self.adj_matrix[u][v] = weight
            self.adj_matrix[v][u] = weight  # For undirected graph

    def add_vertex_data(self, vertex, data):
        if 0 <= vertex < self.size:
            self.vertex_data[vertex] = data

第 3-5 行:起初,邻接矩阵为空,表示图中没有边。此外,顶点一开始没有名称。

第 7-10 行: add_edge 方法用于在无向图中添加一条带权重的边。

第 12-14 行: add_vertex_data 方法用于为顶点命名,例如“A”或“B”。

现在图的创建结构已经到位,我们可以在 Graph 类中实现普里姆算法作为一种方法。

    def prims_algorithm(self):
        in_mst = [False] * self.size
        key_values = [float('inf')] * self.size
        parents = [-1] * self.size

        key_values[0] = 0  # Starting vertex

        print("Edge \tWeight")
        for _ in range(self.size):
            u = min((v for v in range(self.size) if not in_mst[v]), key=lambda v: key_values[v])

            in_mst[u] = True

            if parents[u] != -1:  # Skip printing for the first vertex since it has no parent
                print(f"{self.vertex_data[parents[u]]}-{self.vertex_data[u]} \t{self.adj_matrix[u][parents[u]]}")

            for v in range(self.size):
                if 0 < self.adj_matrix[u][v] < key_values[v] and not in_mst[v]:
                    key_values[v] = self.adj_matrix[u][v]
                    parents[v] = u

第 17 行: in_mst 数组保存当前哪些顶点属于 MST 的状态。最初,没有顶点是 MST 的一部分。

第 18 行: key_values 数组保存从 MST 顶点到 MST 外部每个顶点的当前最短距离。

第 19 行: MST 边存储在 parents 数组中。每条 MST 边通过存储每个顶点的父索引来存储。

第 21 行:为了简单起见,并使此代码像上面的“手动运行”动画/示例一样运行,第一个顶点(索引 0 处的顶点 A)被设置为起始顶点。将索引更改为 4 将从顶点 E 运行普里姆算法,这也同样有效。

第 25 行:找到具有最低键值且尚未属于 MST 的顶点的索引。查看这些对 minlambda 的解释,以更好地理解这一 Python 代码行。

第 32-35 行:将一个新顶点添加到 MST 后(第 27 行),代码的这部分会检查从这个新添加的 MST 顶点是否有可能降低到 MST 外部其他顶点的键值。如果存在这种情况,则相应地更新 key_valuesparents 数组。在动画中,当一个新顶点被添加到 MST 并成为活动(当前)顶点时,这一点就很清楚了。

现在,让我们创建上面“手动运行”中的图,并在其上运行普里姆算法。

示例

Python

class Graph:
    def __init__(self, size):
        self.adj_matrix = [[0] * size for _ in range(size)]
        self.size = size
        self.vertex_data = [''] * size

    def add_edge(self, u, v, weight):
        if 0 <= u < self.size and 0 <= v < self.size:
            self.adj_matrix[u][v] = weight
            self.adj_matrix[v][u] = weight  # For undirected graph

    def add_vertex_data(self, vertex, data):
        if 0 <= vertex < self.size:
            self.vertex_data[vertex] = data

    def prims_algorithm(self):
        in_mst = [False] * self.size
        key_values = [float('inf')] * self.size
        parents = [-1] * self.size

        key_values[0] = 0  # Starting vertex

        print("Edge \tWeight")
        for _ in range(self.size):
            u = min((v for v in range(self.size) if not in_mst[v]), key=lambda v: key_values[v])

            in_mst[u] = True

            if parents[u] != -1:  # Skip printing for the first vertex since it has no parent
                print(f"{self.vertex_data[parents[u]]}-{self.vertex_data[u]} \t{self.adj_matrix[u][parents[u]]}")

            for v in range(self.size):
                if 0 < self.adj_matrix[u][v] < key_values[v] and not in_mst[v]:
                    key_values[v] = self.adj_matrix[u][v]
                    parents[v] = u

g = Graph(8)

g.add_vertex_data(0, 'A')
g.add_vertex_data(1, 'B')
g.add_vertex_data(2, 'C')
g.add_vertex_data(3, 'D')
g.add_vertex_data(4, 'E')
g.add_vertex_data(5, 'F')
g.add_vertex_data(6, 'G')
g.add_vertex_data(7, 'H')

g.add_edge(0, 1, 4)  # A - B
g.add_edge(0, 3, 3)  # A - D
g.add_edge(1, 2, 3)  # B - C
g.add_edge(1, 3, 5)  # B - D
g.add_edge(1, 4, 6)  # B - E
g.add_edge(2, 4, 4)  # C - E
g.add_edge(2, 7, 2)  # C - H
g.add_edge(3, 4, 7)  # D - E
g.add_edge(3, 5, 4)  # D - F
g.add_edge(4, 5, 5)  # E - F
g.add_edge(4, 6, 3)  # E - G
g.add_edge(5, 6, 7)  # F - G
g.add_edge(6, 7, 5)  # G - H

print("Prim's Algorithm MST:")
g.prims_algorithm()
运行示例 »

第 32 行:通过将此行更改为 for _ in range(self.size - 1):,我们可以避免普里姆算法的最后一个循环。这是因为当只有一个顶点尚未包含在 MST 中时,该顶点的父顶点已在 parents 数组中正确设置,因此 MST 实际上在此点已经找到。


普里姆算法的时间复杂度

有关时间复杂度的一般性解释,请访问此页面

设 \(V\) 为我们图中顶点的数量,普里姆算法的时间复杂度为

\[ O( V^2 ) \]

我们得到此时间复杂度的原因是普里姆算法内部的嵌套循环(一个 for 循环包含另外两个 for 循环)。

第一个 for 循环(第 24 行)遍历图中的所有顶点。其时间复杂度为 \(O(V)\)。

第二个 for 循环(第 25 行)遍历图中的所有相邻顶点,以查找具有最低键值且不在 MST 中的顶点,以便它可以是下一个包含在 MST 中的顶点。其时间复杂度为 \(O(V)\)。

在将一个新顶点包含在 MST 中后,第三个 for 循环(第 32 行)会检查所有其他顶点,以查看新添加的 MST 顶点是否有指向 MST 外部顶点的出边,这些边可能导致较低的键值和更新的父关系。此操作的时间复杂度也为 \(O(V)\)。

将时间复杂度组合起来,我们得到

\[ \begin{equation} \begin{aligned} O(V)\cdot (O(V)+O(V)) & = O(V)\cdot (2\cdot O(V)) \\ & = O(V\cdot 2\cdot V) \\ & = O(2\cdot V^2) \\\\ & = O(V^2) \end{aligned} \end{equation} \]

通过使用优先队列数据结构来管理键值,而不是像这里那样使用数组,普里姆算法的时间复杂度可以降低到

\[ O( E \cdot \log{V}) \]

其中 \(E\) 是图中的边数,\(V\) 是顶点数。

使用优先队列实现普里姆算法对于稀疏图是最佳的。当每个顶点只连接到其他少数几个顶点时,该图就是稀疏的。



×

联系销售

如果您想将 W3Schools 服务用于教育机构、团队或企业,请发送电子邮件给我们
sales@w3schools.com

报告错误

如果您想报告错误,或想提出建议,请发送电子邮件给我们
help@w3schools.com

W3Schools 经过优化,旨在方便学习和培训。示例可能经过简化,以提高阅读和学习体验。教程、参考资料和示例会不断审查,以避免错误,但我们无法保证所有内容的完全正确性。使用 W3Schools 即表示您已阅读并接受我们的使用条款Cookie 和隐私政策

版权所有 1999-2024 Refsnes Data。保留所有权利。W3Schools 由 W3.CSS 提供支持