返回

小 试 牛 刀:Python 实现最小生成树——Prim 算法与 Kruskal 算法

闲谈

前言

最小生成树(Minimum Spanning Tree,简称 MST)是图论中的一个经典问题,也是计算机科学中常用的算法。MST 的目标是找到一个生成树,使树中所有边的权重之和最小。生成树是指一个连接图中所有顶点的无环连通子图,最小生成树则是所有生成树中权重最小的一个。

解决最小生成树问题常用的有 Prim 算法和 Kruskal 算法,二者均基于贪心算法。Prim 算法从一个顶点出发,每次选择权重最小的边将新的顶点加入生成树,直到所有顶点都被加入。Kruskal 算法则先将所有边按权重从小到大排序,然后依次选择边加入生成树,直到所有顶点都被加入。

Prim 算法

Prim 算法的思想很简单,即每步都沿着最小权重的边向外扩展生成树。具体步骤如下:

  1. 选择一个顶点作为初始生成树。
  2. 在当前生成树中,找到所有与生成树相邻且尚未加入生成树的顶点。
  3. 从这些顶点中选择一个权重最小的边,将对应的顶点加入生成树。
  4. 重复步骤 2 和 3,直到所有顶点都被加入生成树。

Prim 算法的伪代码如下:

Prim(G, w)
    1. 选择一个顶点作为初始生成树。
    2. while 生成树中顶点数 < G.V:
    3.     在当前生成树中,找到所有与生成树相邻且尚未加入生成树的顶点。
    4.     从这些顶点中选择一个权重最小的边,将对应的顶点加入生成树。
    5. return 生成树

Kruskal 算法

Kruskal 算法与 Prim 算法不同,它先将所有边按权重从小到大排序,然后依次选择边加入生成树,直到所有顶点都被加入。具体步骤如下:

  1. 将所有边按权重从小到大排序。
  2. 从权重最小的边开始,依次选择边加入生成树。
  3. 如果选择的边与当前生成树中的边形成回路,则跳过该边,继续选择下一条边。
  4. 重复步骤 2 和 3,直到所有顶点都被加入生成树。

Kruskal 算法的伪代码如下:

Kruskal(G, w)
    1. 将所有边按权重从小到大排序。
    2. while 生成树中顶点数 < G.V:
    3.     从权重最小的边开始,依次选择边加入生成树。
    4.     如果选择的边与当前生成树中的边形成回路,则跳过该边,继续选择下一条边。
    5. return 生成树

Python 实现

下面给出 Prim 算法和 Kruskal 算法的 Python 实现代码:

import heapq

class Graph:
    def __init__(self, vertices):
        self.V = vertices
        self.graph = [[0 for _ in range(vertices)] for _ in range(vertices)]

    def add_edge(self, u, v, weight):
        self.graph[u][v] = weight
        self.graph[v][u] = weight

    def primMST(self):
        # 存储生成的最小生成树
        mst = []

        # 选择一个顶点作为初始生成树
        visited = [False] * self.V
        visited[0] = True

        # 优先队列,存储当前生成树中顶点到其他顶点的最小权重边
        pq = []
        heapq.heappush(pq, (0, 0))

        # 循环,直到所有顶点都被加入生成树
        while len(mst) < self.V - 1:
            # 获取优先队列中的最小权重边
            weight, u = heapq.heappop(pq)

            # 如果顶点 u 已经加入生成树,则跳过
            if visited[u]:
                continue

            # 将顶点 u 加入生成树
            visited[u] = True

            # 将边 (u, v) 加入最小生成树
            mst.append((u, v))

            # 将顶点 u 的所有边加入优先队列
            for v in range(self.V):
                if self.graph[u][v] > 0 and not visited[v]:
                    heapq.heappush(pq, (self.graph[u][v], u))

        return mst

    def kruskalMST(self):
        # 存储生成的最小生成树
        mst = []

        # 将所有边按权重从小到大排序
        edges = []
        for u in range(self.V):
            for v in range(u + 1, self.V):
                if self.graph[u][v] > 0:
                    edges.append((self.graph[u][v], u, v))

        edges.sort()

        # 并查集,用于判断边是否会形成回路
        parent = [i for i in range(self.V)]

        def find(x):
            if parent[x] != x:
                parent[x] = find(parent[x])
            return parent[x]

        def union(x, y):
            x_root = find(x)
            y_root = find(y)
            parent[y_root] = x_root

        # 循环,直到所有顶点都被加入生成树
        while len(mst) < self.V - 1:
            # 获取权重最小的边
            weight, u, v = edges.pop(0)

            # 如果边 (u, v) 不会形成回路,则将边加入最小生成树
            if find(u) != find(v):
                mst.append((u, v))
                union(u, v)

        return mst

# 测试
g = Graph(5)
g.add_edge(0, 1, 2)
g.add_edge(0, 3, 6)
g.add_edge(1, 2, 3)
g.add_edge(1, 3, 8)
g.add_edge(1, 4, 5)
g.add_edge(2, 4, 7)

primMST = g.primMST()
print("Prim 算法最小生成树:", primMST)

kruskalMST = g.kruskalMST()
print("Kruskal 算法最小生成树:", kruskalMST)

运行结果

Prim 算法最小生成树: [(0, 1), (1, 2), (0, 3), (1, 4)]
Kruskal 算法最小生成树: [(0, 1), (1, 2), (0, 3), (1, 4)]

总结

Prim 算法和 Kruskal 算法都是解决最小生成树问题的经典算法,两种算法各有优缺点。Prim 算法实现简单,但时间复杂度为 O(V^2),而 Kruskal 算法的时间复杂度为 O(E log E),在稀疏图中表现更好。

在实际应用中,可以根据具体情况选择合适的算法。如果图的规模较小,则 Prim 算法更适合;如果图的规模较大且稀疏,则 Kruskal 算法更适合。