目录
一、概念
1、Without any cycle,不能形成闭环;
2、Connect all the vertices,必须连接图结构中的每个顶点,意思是每个顶点,都能通过某种方法连接起来;
3、N Vertices, N-1 Edges,N个顶点数,边的个数是N-1;
最小的概念:
把边对应的全值数相加,称为全值和,不同的树对应不同的全值和。最小生成树对应的就是全值树对应的那棵树。
二、如何构建最小生成树
1、克鲁斯卡尔算法(Kruskal)
2、普里姆算法(Prim)
既然目标是全值和达到最小值,所以每一步都优先选择权值最小的边,这一点两个算法相同。但两个算法在达到这一目标的方法不同。克鲁斯卡尔是直接选择权值最小的边,而普里姆算法是从顶点出发,选择与顶点相邻、权值最小的边。
1、克鲁斯卡尔算法(Kruskal)
图1
将图中所有的边都取出放入一个列表中,并按照权值由小到大的排列。按列表的顺序,将每一条边回贴到图中,每回贴一条边,就判断每条边的状态,看看是否能形成环,如果没有形成环,这条边就会被保留,如果形成环,这条边就会被放弃,继续判断列表中的下一条边。直到选择了N-1条边,这时最小生成树就完成。
如何判断欲加入的一条边是否与生成树中已保留的边形成回路呢?有一个方法是:并查集(标号法)(合并并查找集合)(不是一家人,不进一家门)。
并查集(下方第二个链接,6:56):
S1:将图各边按照权值进行排序;并且给每个顶点都加上一个序号(表示该顶点属于标号所示的联通分量);接下来就要按权值大小的顺序选边,然后看这个边对应的两个顶点,看这两个顶点的序号是否一样。
S2:选中处于同一条边上的两个顶点(按照权值排序从小到大选择),如果两个顶点的标号值不同,则将标号修改为同一个类的标号值(把大的标号改为小的标号);
S3:如果标号值相同,表示回路出现,需要把这一条边舍去,选择下一条权重对应的边。直到所有顶点的标号值一直为止,有n-1条边为止。
2、普里姆算法(Prim)
同样优先选择权值小的边,但是从顶点出发,优先选择连接两个顶点集合的所有边中权值最小的。
图2
这两个顶点集合,一个是已选顶点集合(已经确定最小生成树连接方式的顶点,图2左边绿色区域),一个时是未选顶点集合(未确定顶点连接方式的所有顶点构成的未选顶点集)。普里姆算法就是在连接这两个集合之间的所有边中连接最小的边。如图2,接下来连接蓝色和绿色的线就是7-6之间权重为1的这条,随后图2变为图3.
图3
具体实现过程,就是通过不断更新三个列表信息。第一个列表,存储该顶点是否已被选取的信息,如果该顶点以确定了最小生成树的连接方式,则为T,否则为F,列表中所有为T的顶点,就构成了前面所说的已选顶点,初始状态下,所有顶点都为F;第二个列表,存储某一时刻连接已选顶点连接未选顶点中,所有边中权值最小的那一条边,初始状态下都为无穷大(inf);最小权值边,两端顶点信息就存储在第三个列表中,由于两个顶点确定一条边,所以初始状态都为-1,表示不存在的一条线。
图4
具体操作过程如下,直接看后面括号部分吧:
以顶点0为起点(确定一个起点,执行下面的三个操作),
1、Update ,更新信息,当 已选顶点中每加入一个新的顶点,更新所有与该顶点相连的顶点信息,如果连接边的权值小于对应顶点列表(minDist)中的值,则更新列表值为该边的权值,同时更新(parent)记录改边,反之则不变(找到与此时起点相连的点,在表中更新它的信息,第二列表写两点间线的权值,第三列表写此时起点对应的数字)
2、Scan,扫描minDist,找到最小值对应的顶点(找到该起点连接其他点对应的最小权值边,通过扫描第二列表)
3、Add,将找到的最小权值边的点,与此时对应的点在图中连接起来,创建最小生成树的一条边(将第二列表扫描到的点与此时起点连接起来,作图就连接成了一条最小生成树,这三步走完,这个点对应的数列就会变化,第一行变作T,第二行变作-,第三行变作此时起点对应的数值;当然,变化了后的扫描,当然只在第一行为F的地方扫啦。接着,用这个被选中的顶点,继续第一步重复(?也有更复杂的情况,比如图5,此时这个被选中的顶点为8,但是8明显不能作下一个起点,所以就莫名变成了3为起点))。
图5
三、克鲁斯卡尔python代码实现
# 找这个顶点的标号
def find(x, pres): # x是这条边的一个顶点,pres是它的标号集
root = pres[x]
return root
# 更改顶点的标点
def change(x, y, pres):
"""
合并两个元素(合并两个集合)
:return: None
"""
h1, h2 = find(x, pres), find(y, pres)
# 当两个元素不是同一组的时候才合并
if h1 != h2:
if pres[h1] < pres[h2]: # 把标号往小的改
pres[h2] = pres[h1]
else:
pres[h1] = pres[h2]
def kruskal(n, edges):
"""
kruskal算法
:param n: 结点数
:param edges: 带权边集
:return: 构成最小生成树的边集
"""
# 初始化:标点集pres = [0, 1, 2, 3, 4, 5];
pres = [e for e in range(n)]
# 边从小到大排序
edges = sorted(edges, key=lambda x: x[-1]) # sorted用于排序
print("edges = ")
print(edges)
mst_edges, num = [], 0
for edge in edges:
if find(edge[0], pres) != find(edge[1], pres):
mst_edges.append(edge) # 如果两个顶点的标号不相同,那么就把这条边加入树中
change(edge[0], edge[1], pres) # 更改顶点的标点
num += 1
else:
continue
if num == n-1:
break
return mst_edges
# !!数据 采用mst图,需要更改
edges = [
[0, 1, 6],
[0, 2, 1],
[0, 3, 5],
[2, 1, 5],
[2, 3, 5],
[2, 4, 5],
[2, 5, 4],
[1, 4, 3],
[4, 5, 6],
[5, 3, 2]
]
# !!结点数(顶点数),需要更改
n = 6
mst_edges = kruskal(n, edges)
print('edges:')
for e in mst_edges:
print(e)
print('Total cost of MST:', sum([e[-1] for e in mst_edges]))
print('Maximum cost of MST:', max([e[-1] for e in mst_edges]))
# std print
#
# edges:
# [0, 2, 1]
# [5, 3, 2]
# [1, 4, 3]
# [2, 5, 4]
# [2, 1, 5]
# Total cost of MST: 15
# Maximum cost of MST: 5
学习对象:
【最小生成树(Kruskal(克鲁斯卡尔)和Prim(普里姆))算法动画演示】 https://www.bilibili.com/video/BV1Eb41177d1/?share_source=copy_web&vd_source=98fbab4e0ff3ef4e18cd477db479634d
【图的最小生成树克鲁斯卡尔(Kruskal)算法-理论+代码】 https://www.bilibili.com/video/BV1d34y1R7xA/?share_source=copy_web&vd_source=98fbab4e0ff3ef4e18cd477db479634d