数据结构与算法 —— 最小生成树Prim算法和Kruskal算法详细图解以及python实现

目录

前言

1. 介绍

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

2.1 Prim算法历史

2.2 Prim算法的基本思路

2.3 Prim算法图解

2.4 Prim算法(python)实现

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

3.1 Kruskal算法的基本思路

3.2 Kruskal算法图解

3.3 Kruskal算法(python)实现

4. 最小生成树和最短路径的区别

5. 结尾


前言

上一章我们整理了最短路径中的Dijkstra算法
现在我们整理最小生成树中Prim算法(普里姆算法)和Kruskal算法(克鲁斯卡尔算法)。


1. 介绍

最小生成树也是图论中常见问题,其指的是在一个图中找到一个连通子图,使得所有节点都能够互相到达,且总权值最小。

最小生成树的例子在生活中也很常见,比如城市的公交线路系统需要规划如何走完一趟路径和是最短的,或者快递公司如何给不同住址的客户送货才能是最短的路径。


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

2.1 Prim算法历史

普里姆算法最初由捷克数学家沃伊捷赫·亚尔尼克于1930年发现,后来在1957年由美国计算机科学家罗伯特·普里姆独立发现,1959年艾兹格·迪科斯彻也再次发现了该算法。因此,普里姆算法在某些情况下也被称为DJP算法、亚尔尼克算法或普里姆-亚尔尼克算法。

2.2 Prim算法的基本思路

首先选择一个起始节点,以这个节点将图分成两个集合。一个已选集合,一个未选集合。把起始节点加入已选集合。

然后从已选节点集合出发,不断选择权值最小的边连接到未选节点集合中的节点,并将其加入已选节点集合。

不断循环此过程,直到所有节点都被加入到已选节点集合中,形成最小生成树。

这就是Prim算法的基本思路。

简单来说,就是从一个点开始,不断寻找距离最短的边,将其加入最小生成树中,并将与之相连的未访问点所构成的边加入集合中,直到所有节点都被访问过为止。

2.3 Prim算法图解

在下面这个图的基础上,我们可以一步一步来分析Prim 算法的过程。

我们的目的是用此算法判断出构成最小生成树的所有边。

 

 接下来一样的操作,就不在图中标注文字解释了。

算法完成,我们得到了构成最小生成树的所有边。

2.4 Prim算法(python)实现

def prim_algorithm(graph):
    num_vertices = len(graph)
    
    # 初始化集合
    selected = set()
    selected.add(list(graph.keys())[0])  # 从第一个顶点开始
    unselected = set(graph.keys()) - selected
    
    # 初始化最小生成树结果
    minimum_spanning_tree = []

    while unselected:
        min_weight = float('inf')
        start_vertex = None
        end_vertex = None

        # 遍历已选集合中的每个顶点
        for vertex in selected:
            # 遍历未选集合中的每个顶点
            for neighbor, weight in graph[vertex].items():
                if neighbor in unselected:
                    # 找到权值最小的边
                    if min_weight > weight:
                        min_weight = weight
                        start_vertex = vertex
                        end_vertex = neighbor

        # 将找到的最小权值边添加到最小生成树结果中
        minimum_spanning_tree.append((start_vertex, end_vertex, min_weight))
        selected.add(end_vertex)
        unselected.remove(end_vertex)

    return minimum_spanning_tree

graph = {
    'A': {'B': 2, 'C': 9},
    'B': {'A': 2, 'D': 4, 'E': 8},
    'C': {'A': 9, 'E': 10, 'F': 3},
    'D': {'B': 4, 'E': 1, 'G': 5},
    'E': {'B': 8, 'C': 10, 'D': 1, 'F': 11, 'G': 6, 'H': 12},
    'F': {'C': 3, 'E': 11, 'H': 17},
    'G': {'D': 5, 'E': 6},
    'H': {'E': 12, 'F': 17},
}

result = prim_algorithm(graph)
print(result)

实现了一个上面举例子用的图,运行此程序会打印出:

[('A', 'B', 2), ('B', 'D', 4), ('D', 'E', 1), ('D', 'G', 5), ('A', 'C', 9), ('C', 'F', 3), ('E', 'H', 12)]

前两个字母表示最小生成树中的一条边,后面的数字代表这条边的权值。可以与上面的图解结合起来看。 


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

3.1 Kruskal算法的基本思路

该算法是先取出所有的边,按其权值从小到大的顺序排列,然后不断取出权值最小的边放入图中,一共取n-1条边(n为节点数)。但是每次放入一条边都要判断是否形成了环。如果没有形成环,则将该边纳入最小生成树中。如果形成了环,则舍弃这条边,继续取下一条边。

3.2 Kruskal算法图解

首先取出图中所有的边,并且按其权值从小到大排序。

 

此时,边的数量 = n-1,算法结束。同样也得到了一个最小生成树。 

3.3 Kruskal算法(python)实现

def find(parent, vertex):
    if parent[vertex] == vertex:
        return vertex
    return find(parent, parent[vertex])

def union(parent, rank, vertex1, vertex2):
    root1 = find(parent, vertex1)
    root2 = find(parent, vertex2)

    if root1 != root2:
        if rank[root1] > rank[root2]:
            parent[root2] = root1
        else:
            parent[root1] = root2
            if rank[root1] == rank[root2]:
                rank[root2] += 1

def kruskal_algorithm(graph):
    # 初始化结果
    minimum_spanning_tree = []

    # 初始化并查集
    parent = {vertex: vertex for vertex in graph.keys()}
    rank = {vertex: 0 for vertex in graph.keys()}

    # 获取所有的边
    edges = []
    for vertex, neighbors in graph.items():
        for neighbor, weight in neighbors.items():
            edges.append((vertex, neighbor, weight))

    # 按权值排序边
    edges.sort(key=lambda edge: edge[2])

    # 不断取出权值最小的边并判断是否形成环
    for edge in edges:
        vertex1, vertex2, weight = edge
        if find(parent, vertex1) != find(parent, vertex2):
            union(parent, rank, vertex1, vertex2)
            minimum_spanning_tree.append(edge)

        if len(minimum_spanning_tree) == len(graph) - 1:
            break

    return minimum_spanning_tree

graph = {
    'A': {'B': 2, 'C': 9},
    'B': {'A': 2, 'D': 4, 'E': 8},
    'C': {'A': 9, 'E': 10, 'F': 3},
    'D': {'B': 4, 'E': 1, 'G': 5},
    'E': {'B': 8, 'C': 10, 'D': 1, 'F': 11, 'G': 6, 'H': 12},
    'F': {'C': 3, 'E': 11, 'H': 17},
    'G': {'D': 5, 'E': 6},
    'H': {'E': 12, 'F': 17},
}

result = kruskal_algorithm(graph)
print(result)

find(parent, vertex)函数:这个函数的作用是找到给定顶点所属集合的代表元素。

union(parent, rank, vertex1, vertex2)函数:这个函数的作用是将两个不同集合合并成一个集合。 

运行上面程序之后会打印:

[('D', 'E', 1), ('A', 'B', 2), ('C', 'F', 3), ('B', 'D', 4), ('D', 'G', 5), ('A', 'C', 9), ('E', 'H', 12)]

可以发现与我们图解的答案包括Prim算法的答案是一致的。

4. 最小生成树和最短路径的区别

最小生成树和最短路径问题在目标、应用场景和解决方法上都有所不同。最小生成树关注的是连接所有顶点的最小总权值,而最短路径关注的是在带权图中找到顶点之间的最短路径。

结合图解来看会更加清晰(选择节点D为起始节点):


5. 结尾

到这里为止,我们已经整理了

图的基本结构

深度优先搜索,广度优先搜素

最短路径Dijkstra算法

最小生成树Prim算法和Kruskal算法。

基本上图的内容已经整理的差不多了。下面会整理贪心算法(greedy algorithm)的内容。

如果有地方讲的不对或者大家有什么疑问,欢迎留言或者私信我!

  • 6
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Prim算法实现: ```Python from heapq import heappush, heappop # 堆优化 from collections import defaultdict def prim(graph, start): mst = defaultdict(set) # 存储最小生成树的结果 visited = set([start]) # 已经访问过的节点 edges = [ (cost, start, to) for to, cost in graph[start].items() ] # 从起始节点开始的所有边 # 将边按照权值排序 edges.sort() # 遍历每一条边 while edges: cost, frm, to = heappop(edges) # 取出当前最小的边 if to not in visited: # 如果该节点未访问过,加入最小生成树 visited.add(to) mst[frm].add(to) for to_next, cost in graph[to].items(): # 将新加入的节点的边加入边集中 if to_next not in visited: heappush(edges, (cost, to, to_next)) return mst ``` Kruskal算法实现: ```Python def kruskal(graph): mst = defaultdict(set) # 存储最小生成树的结果 vertices = set(graph.keys()) # 所有节点 edges = [ (graph[v1][v2], v1, v2) for v1 in vertices for v2 in graph[v1] ] # 所有边 # 将边按照权值排序 edges.sort() # 遍历每一条边 for cost, frm, to in edges: if to not in vertices: # 如果该边的终点不在当前最小生成树中,加入该边 continue vertices.remove(to) mst[frm].add(to) mst[to].add(frm) return mst ``` 其中,图的数据结构可以使用字典来表示,例如: ```Python graph = { 'A': {'B': 2, 'D': 6}, 'B': {'A': 2, 'C': 3, 'D': 8}, 'C': {'B': 3}, 'D': {'A': 6, 'B': 8}, } ``` 其中,每个键(例如'A')表示一个节点,对应的值(例如{'B': 2, 'D': 6})表示与该节点相连的边及其权值。例如,图中节点'A'与节点'B'之间的边权为2,节点'A'与节点'D'之间的边权为6。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值