数据结构(十):最小生成树

简介: 最小生成树是带权无向连通图中权值最小的生成树,根据图中生成树定义可知, 个顶点的连通图中,生成树中边的个数为 ,向生成树中添加任意一条边,则会形成环。
img_98c13501afb7a2710dd7784e300e0162.jpe

最小生成树是带权无向连通图中权值最小的生成树,根据中生成树定义可知,|V| 个顶点的连通图中,生成树中边的个数为 |V|-1,向生成树中添加任意一条边,则会形成环。生成树存在多种,其中权值之和最小的生成树即为最小生成树。

最小生成树保证最小权值是固定的,但是最小生成树可能有多个。

A 为最小生成树 MST 的一个真子集,即 A 的顶点集合和边集合都是 MST 的顶点和边集合的子集,构造最小生成树过程为向 A 中添加顶点和边,添加的原则有两种:

  1. 选择 A 的边集合外,权值最小的边,加入到 A

添加边的过程需要避免形成环。

  1. 选择 A 的顶点集合外,距离 A 最近的顶点,加入到 A

距离 A 最近的点,即和 A 中的顶点形成最小权值边的非 A 中的某个顶点。

kruskal 算法

kruskal 算法即为上述第一种原则,通过选择图中的最小权值边来构造最小生成树,过程中需要注意避免形成环。

算法过程
  1. 对边集合进行排序
  2. 选择最小权值边,若不构成环,则添加到集合 A
  3. 重复执行步骤 2,直到添加 |V|-1 条边
演示示例
img_253de4c743a2a930d16ae86070ad7c99.png
graph

step 1:
最小权值边为顶点 7、8 形成的边

img_74b9bba41ef9ad5ffe8bab3b906af1d5.png

step 2:
最小权值边为顶点 3、9 形成的边

img_4a9c441f41204b8dd6cc42a3c5b3ea65.png

step 3:
最小权值边为顶点 6、7 形成的边

img_cd8d595fd6ed3b74aa086cc6e568e5a9.png

step 4:
最小权值边为顶点 3、6 形成的边

img_c1b26b41a97f84fdb9a4b61b62337c73.png

step 5:
最小权值边为顶点 1、2 形成的边

img_8238981b8e704fc3f44c2730d2593de0.png

step 6:
最小权值边为顶点 3、4 形成的边

img_5b2ad7dd1a800288db3c2b4faeff2702.png

step 7:
最小权值边为顶点 1、8 形成的边

img_495469e1193af065961b60e000137199.png

step 8:
最小权值边为顶点 4、5 形成的边

img_0da6a916dd132fc4c5bcea15d7baf9d4.png

最小生成树的权值之和为 37

算法示例

这里使用邻接表作为图的存储结构

  1. kruskal 算法示例
def kruskal(graph):
    edges, vertices = getEdgesFromAdjacencyList(graph), [i for i in range(graph.number)]
    sort(edges, 0, len(edges) - 1)
    weightSum, edgeNumber = 0, 0
    while edgeNumber < graph.number - 1:
        edge = edges.pop()
        beginOrigin, endOrigin = origin(vertices, edge.begin - 1), origin(vertices, edge.end - 1)
        if (beginOrigin != endOrigin): # whether the two vertices belong to same graph
            vertices[beginOrigin] = endOrigin  # identify the two vertices in the same sub graph
            weightSum, edgeNumber = weightSum + edge.weight, edgeNumber + 1  # calculate the total weight

这里使用 getEdgesFromAdjacencyList 函数完成邻接表到边集合的转换,使用快排 sort 完成对边集合的排序,使用 origin 函数返回每个子图的根。

kruskal 算法设定最初每个顶点都是一个子图,每个子图都有一个根,或者称之为出发点,每个加入的顶点都保留一个指向上一个顶点的引用,并最终追溯到该子图的根顶点,所以可以通过判断两个顶点指向的根顶点是否相同,来判断两顶点是否属于同一个子图。

  1. 邻接表转边集合
def getEdgesFromAdjacencyList(graph):
    edges = []
    for i in range(graph.number):
        node = graph.list[i]
        while node:
            edge, node = Edge(i + 1, node.index, node.weight), node.next
            edges.append(edge)
    return edges

因为使用邻接表向边进行转化,且后续只对边集合进行处理,所以在测试时候,无向图中的每条边,只需要记录一次即可,不需要对于边的两个顶点,分别记录一次。

  1. 判断两个顶点是否属于同一个子图,避免添加边后形成环
def origin(vertices, index):
    while vertices[index] != index:
        index = vertices[index]
    return index

该函数返回顶点 index 所属子图的根顶点,其中 vertices[index] 位置上存储的是顶点 index 的上一个顶点,每个子图中,根顶点的上一个顶点为自身。

性能分析

kruskal 算法中使用 getEdgesFromAdjacencyList 函数完成邻接表向边集合的转换,函数内部存在两层循环,访问邻接表中每个顶点的相邻顶点,复杂度为 O(log|E|)。使用快排对边集合进行排序,时间复杂度为 O(|E|log |E|),因为 |E| \lt |V|^2,所以快排时间复杂度可以表述为 O(|E|log |V|)kruskal 算法中 while 循环取最小权值边,并对边的两个顶点执行 origin 函数判断是否属于同一个子图,时间复杂度为 O(|E|log |V|)。所以 kruskal 算法的时间复杂度为 O(|E|log |V|)

prim 算法

kruskal 算法的过程为不断对子图进行合并,直到形成最终的最小生成树。prim 算法的过程则是只存在一个子图,不断选择顶点加入到该子图中,即通过对子图进行扩张,直到形成最终的最小生成树。

扩张过程中选择的顶点,是距离子图最近的顶点,即与子图中顶点形成的边是权值最小的边。

算法过程
  1. 按照距离子图的远近,对顶点集合进行排序
  2. 选择最近的顶点加入到子图中,并更新相邻顶点对子图的距离
  3. 重复执行步骤 2,直到顶点集合为空
演示示例
img_253de4c743a2a930d16ae86070ad7c99.png
graph

这里不妨以顶点 5 作为子图中的第一个顶点

step 1:
距离子图的最近顶点为 4

img_5f12e770d9ba0b969acdb2bf2a9d1190.png

step 2:
距离子图的最近顶点为 3

img_1c822f931128a34bfb74bd9f40d985f5.png

step 3:
距离子图的最近顶点为 9

img_3780fb29adc28915c04939617a9f8d3e.png

step 4:
距离子图的最近顶点为 6

img_a08e4473e8c91351a29942d6b0ba5500.png

step 5:
距离子图的最近顶点为 7

img_73d50623526ea0adf653da9457eda9f0.png

step 6:
距离子图的最近顶点为 8

img_33ba24f48b680e88d4831adc868faf7d.png

step 7:
距离子图的最近顶点为 2

img_45d1da9b06430a726b704969cbfe53d3.png

step 8:
距离子图的最近顶点为 1

img_8691a8ed9c598c3c851d1f29a9145a35.png

最小生成树的权值之和为 37

算法示例

这里使用邻接表作为图的存储结构

  1. prim 算法示例
def prim(graph, index):
    vertices, verticesIndex = [{'index': i, 'weight': None} for i in range(graph.number)], [i for i in range(graph.number)]
    weightSum, vertices[index - 1]['weight'] = 0, 0
    heapSort(vertices, verticesIndex)
    while len(vertices) > 0:
        swapVertices(vertices, verticesIndex, 0, -1)
        vertex = vertices.pop()
        transformToHeap(vertices, verticesIndex, 0, len(vertices))
        weightSum = weightSum + vertex['weight']
        updateVertices(graph, vertices, verticesIndex, vertex['index'])

这里使用 vertices 列表存储每个顶点元素,每个元素包括两个属性,index 为顶点下标,weight 为顶点距离子图的大小。算法中使用 verticesIndex 列表存储每个顶点元素在 vertices 列表中的下标位置。使用 heapSort 堆排序对每个顶点到子图的距离进行排序,即对 vertices 列表进行排序,使用堆排序内的 transformToHeap 函数调整 vertices 列表为小顶堆。当添加新顶点到子图后,使用 updateVertices 函数完成对相邻顶点的距离更新。

因为对 vertices 列表排序后,每个顶点元素在 vertices 列表的下标值不能表示该顶点的编号,而后续添加新顶点后,在更新相邻顶点距离的操作中,为了避免查找相邻顶点而遍历整个列表,需要根据顶点编号进行直接访问相邻顶点,所以借助 verticesIndex 列表存储每个顶点元素在 vertices 列表中的位置。例如要更新顶点 v 的距离,则 verticesIndex[v] 值为顶点 vvertices 列表中的位置,v 顶点元素即为 vertices[verticesIndex[v]]

  1. 交换堆顶元素
def swapVertices(vertices, verticesIndex, origin, target):
    vertices[origin], vertices[target] = vertices[target], vertices[origin]
    verticesIndex[vertices[origin]['index']], verticesIndex[vertices[target]['index']] = origin, target

vertices 列表调整为小顶堆之后,将列表首、尾元素交换,则列表尾元素即为距离子图最近的顶点元素。

  1. 添加顶点到子图中后,更新相邻顶点到子图的距离
def updateVertices(graph, vertices, verticesIndex, index):
    node = graph.list[index]
    while node:
        if verticesIndex[node.index - 1] == -1:
            node = node.next
            continue
        vertex = vertices[verticesIndex[node.index - 1]]
        if not vertex['weight'] or (vertex['weight'] and vertex['weight'] > node.weight):
            vertex['weight'] = node.weight
            pos = verticesIndex[vertex['index']]
            while pos > 0 and (not vertices[(pos - 1) // 2]['weight'] or vertices[pos]['weight'] < vertices[(pos - 1) // 2]['weight']):
                swapVertices(vertices, verticesIndex, pos, (pos - 1) // 2)
                pos = (pos - 1) // 2
        node = node.next

对每一个相邻顶点,如果不在子图中,则判断是否更新到子图的距离。

性能分析

prim 算法中构造顶点列表的时间复杂度为 O(|V|)。使用堆排序对顶点列表进行排序,时间复杂度为 O(|V|log |V|)prim 算法中 while 循环取最近顶点元素,并调整元素取出后列表的堆结构,所以总体的调整复杂度为 O(|V|log |V|);同时循环结构内执行 updateVertices 函数,更新每个取出顶点的相邻顶点距离值,所以总体的更新顶点数为 O(|E|),因为每个顶点更新距离后,需要调整堆结构为小顶堆,所以总体的复杂度为 O(|E|log |V|)。所以prim 算法的时间复杂度为 O(|E|log |V|)

代码及测试 github 链接:最小生成树

相关文章
|
4天前
|
存储 机器学习/深度学习 算法
上机实验三 图的最小生成树算法设计 西安石油大学数据结构
上机实验三 图的最小生成树算法设计 西安石油大学数据结构
16 1
|
10月前
|
算法 Java
数据结构(13)最小生成树JAVA版:prim算法、kruskal算法
13.1.概述 最小生成树,包含图的所有顶点的一棵树,树的边采用包含在图中的原有边中权重和最小的边。翻译成人话就是遍历一遍全图所有顶点的最短路径,这条路径就叫最小生成树。 最小生成树存在和图是连通图互为充要条件,顶点都不连通,肯定不可能有路能遍历一遍全图。 求解最小生成树有两种常用算法:
107 0
|
10月前
|
存储 算法 Java
【Java高阶数据结构】并查集-最小生成树
Java高阶数据结构 & 并查集 & 最小生成树 1. 并查集 1.1 并查集的原理 在一些应用问题中,我们常常会遇到一类问题 一开始是一个人 后来新增的人可能与这个人有关系,也可能与这个人无关系。 一个人与一个人有关系,这个人与另一个人也有关系,那么三人都有关系。 有关系的和没关系的之间是不同的类别。
83 0
|
11月前
|
算法
大话数据结构--最小生成树的基本概念
大话数据结构--最小生成树的基本概念
107 0
|
算法 C语言 C++
数据结构 图连通与最小生成树(下)
数据结构 图连通与最小生成树(下)
106 0
|
机器学习/深度学习 存储 算法
数据结构 图连通与最小生成树(上)
数据结构 图连通与最小生成树
93 0
|
存储 人工智能 算法
数据结构学习笔记——图的应用1(最小生成树、最短路径)
数据结构学习笔记——图的应用1(最小生成树、最短路径)
数据结构学习笔记——图的应用1(最小生成树、最短路径)
|
算法 调度
数据结构与算法—最小生成树(Prim算法和Kruskal算法算法详解)
通俗易懂的讲就是最小生成树包含原图的所有节点而只用最少的边和最小的权值距离。因为n个节点最少需要n-1个边联通,而距离就需要采取某种策略选择恰当的边。
278 0
数据结构与算法—最小生成树(Prim算法和Kruskal算法算法详解)
|
算法
【数据结构】最小生成树的概念
【数据结构】最小生成树的概念
267 0
【数据结构】最小生成树的概念
|
存储 算法
数据结构面试之九——图的常见操作3之最小生成树
最小生成树——包含带权图中的全部顶点并不能形成环,且权值之和最小的图。 求解最小生成树的方法包括:Prim算法和Kruskal算法。 对于Prim算法思想:1)从源结点集中选定一个源结点(初始源节点集合中只有设定一个结点);2)从剩余结点中选择与源节点集有连接的且权值最小的边。将该源节点加入源节点集合中。然后迭代执行1),2)。
200 0

热门文章

最新文章