Fork me on GitHub

最小生成树之普里姆与克里斯特尔算法

最小生成树是经过无向连通带权图进行转换生成的,在一开始,先了解下图这个数据结构及相关概念。
图可用二元组表示,G(V,E),其中V表示顶点的非空有限集合,E表示边的有限集合。

有向图与无向图

以下图G1则为有向图,G2为无向图,G1中每条边都是有方向的,在有向图中<1,2>、<2,1>是不同的两个方向边,无向图则代表同一边。

连通、连通图、连通分量

连通:在无向图中,若从顶点Vi到顶点Vj之间有路径,则这两个顶点是连通的。
连通图:如果图中任意一对顶点是连通的则为连通图。
连通分量,非连通图的每一个连通部分叫做连通分量,连通图的连通分量就是其本身。

带权图


每条边都有其在特定场景下的值,比如长度,比如粗细等,具体的实例有供水管网中的管道长度,网络建设中的阻抗等。

生成树

一个连通图G的子图如果是一个包含了G的所有顶点的树,则该子图为G的生成树,有两种算法可以得到生成树,一种是深度优先搜索算法,一种是广度优先搜索算法,深度优先得到的生成树为DFS生成树,广度优先为BFS生成树。具体可以参考深度优先与广度优先算法

最小生成树

最小生成树是生成树的一种特例,‘最小’指的是生成树所有的边加起来的权值最小,所以最小生成树是在带权图中生成的。

最小生成树之普里姆Prim算法

算法思想:创建并扩展一棵树,为它添加新的树枝
算法核心:设U为最小生成树的顶点集,V为图G的顶点集合,目的是让U中的顶点与V-U中的顶点结合找到一条最短边来扩充生成树T
算法步骤说明:
下图为我构造的一个无向连通带权图G,有6个顶点,8条边
vertexs = ['A', 'B', 'C', 'D', 'E', 'F']
edges = [('A', 'B', 2),('A', 'C', 1),('A', 'D', 5),('B', 'D', 4),('C', 'E', 3),('C', 'F', 4),('D', 'E', 2),('E', 'F', 7)] // 第三个数值为边的权值

通过普里姆算法思想,下图说明了创建并扩展一棵树的过程。

整个构建的过程为,初始所构建的最小生成树为空,只有一个顶点A,然后在已有图G中的所有边中进行查找,查找A顶点相关的边,且此边的权值最小,于是找到了('A','C',1),将('A','C',1)的边与C顶点加入到最小生成树,继续往下找,现在最小生成树中已经有了顶点A,C,边('A','C',1),于是再进行一次循环,查找图G中所有边中,与顶点A,C形成的边的权值最小,经过比较,发现('A','B',2)满足,于是将('A','B',2)与顶点B加入最小生成树,然后继续往下找,直到将图G中的所有顶点都加入到最小生成树中,则查找结束。为此我们整个查找过程就需要图G顶点的个数减1次的循环,因此初始顶点A已经加入最小生成树中,于是只需要顶点个数减1次查找次数即可。
以下是普里姆算法的Python代码:

# 构建一个树
# 入选顶点集与入选边集
tree_vertex = ['A']
tree_edges = []

for i in range(len(vertex) - 1):
    # 最小权值的边
    mini_weight = ()
    for edge in edges:
        # 如果边中有两个顶点在入选顶点集中,则此条边需要忽略
        if edge[0] in tree_vertex and edge[1] in tree_vertex:
            continue
        # 如果边有一个顶点在入选顶点集中,则此条边需要进行比较权值
        if edge[0] in tree_vertex or edge[1] in tree_vertex:
            if not mini_weight:
                mini_weight = edge
            elif mini_weight[2] > edge[2]:
                mini_weight = edge
    # 将边和点添加进最小生成树
    tree_edges.append(mini_weight)
    if mini_weight[0] not in tree_vertex:
        tree_vertex.append(mini_weight[0])
    if mini_weight[1] not in tree_vertex:
        tree_vertex.append(mini_weight[1])
 
print(tree_edges)  # [('A', 'C', 1), ('A', 'B', 2), ('C', 'E', 3), ('D', 'E', 2), ('C', 'F', 4)]
print(tree_vertex)  # ['A', 'C', 'B', 'E', 'D', 'F']

简化查找,将边按照权值排序

点击查看代码
for i in range(0, len(vertex) - 1):
    for edge in edges:
        # 形成回路,抛弃这条边
        if edge[0] in tree_vertex and edge[1] in tree_vertex:
            continue
        # 边中包含入选顶点,并且此条边为最小权值
        if edge[0] in tree_vertex or edge[1] in tree_vertex:
            tree_edges.append(edge)
            if edge[0] in tree_vertex:
                tree_vertex.append(edge[1])
            elif edge[1] in tree_vertex:
                tree_vertex.append(edge[0])
            break
print(tree_edges)  # [('A', 'C', 1), ('A', 'B', 2), ('C', 'E', 3), ('D', 'E', 2), ('C', 'F', 4)]
print(tree_vertex)  # ['A', 'C', 'B', 'E', 'D', 'F']

最小生成树之克鲁斯卡尔Kruskal算法

算法思想:扩展一棵树的集,并构成一棵生成树
算法核心:设U为最小生成树的顶点集,V为图G的顶点集合,目的是让U中的顶点与V-U中的顶点结合找到一条最短边来扩充生成树T
算法步骤说明:
继续使用上图的无向连通带权图G来进行说明:

整个构建的过程为,初始所构建的最小生成树包含了图G的所有顶点,选中某一顶点(假设为A),然后在已有图G中的所有边(经过了按权值排序)中进行查找,查找A顶点相关的边,且此边的不构成环路,于是找到了('A','C',1),将('A','C',1)的边加入到最小生成树,继续往下找,再进行一次循环,找到('A','B',2),发现('A','B',2)与最小生成树中的边,并未构成环路,于是加入,继续往下找,直到最小生成树中的所有顶点都在一个连通分量中,则查找结束。
以下是克鲁斯卡尔算法的Python代码:

点击查看代码
vertexs = ['A', 'B', 'C', 'D', 'E', 'F']
edges = [('A', 'C', 1), ('A', 'B', 2), ('D', 'E', 2), ('C', 'E', 3), ('B', 'D', 4), ('C', 'F', 4), ('A', 'D', 5),
         ('E', 'F', 7)]

# 最小生成树
tree_edges = []
# 顶点的标记,初始顶点的标记都不相同
vertexs_sign = {'A': 1, 'B': 2, 'C': 3, 'D': 4, 'E': 5, 'F': 6}

for edge in edges:
    # 判断edge与最小生成树中的边是否构成环
    if vertexs_sign[edge[0]] == vertexs_sign[edge[1]]:
        # 形成了环,忽略此条边
        continue
    if vertexs_sign[edge[0]] != vertexs_sign[edge[1]]:
        tree_edges.append(edge)
        # 将加入的边的顶点的相同标记的顶点都设置为一样
        temp = vertexs_sign[edge[0]]
        for key in vertexs_sign:
            if vertexs_sign[key] == temp:
                vertexs_sign[key] = vertexs_sign[edge[1]]
        print(vertexs_sign)
    if len(tree_edges) == len(vertexs) - 1:
        break
print(tree_edges)  # [('A', 'C', 1), ('A', 'B', 2), ('D', 'E', 2), ('C', 'E', 3), ('C', 'F', 4)]
print(vertexs_sign)  # {'A': 6, 'B': 6, 'C': 6, 'D': 6, 'E': 6, 'F': 6}

总结

1、图是一种相较于树的数据结构,图分为有向图与无向图,分为连通图与非连通图,分为带权图与不带权图
2、图G的一个包含所有顶点的连通子图则为图G的生成树,带权图的权值加起来最小则为最小生成树
3、最小生成树有两种算法,Prim(普利姆)算法与Kruskal(克鲁斯卡尔)算法,为了更加方便实现算法,将图的边集进行按权值排序,可以更快生成最小生成树

posted @   三脚半猫  阅读(187)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
点击右上角即可分享
微信分享提示