文章目录
- 前言
- 一、最小生成树
- 树的一些概念
- 关键特性
- 最小生成树和最短路径的主要区别
- 常用算法
- 1. Kruskal算法(适合点多边少的图)
- 2. Prim算法(适合边多点少的图)
- 二、示例
- 三、代码实现----Matlab
- 四、代码实现----python
- 1. Kruskal算法
- 2. Prim算法
- 总结
前言
通过模型算法,熟练对Matlab和python的应用。
学习视频链接:
https://www.bilibili.com/video/BV1EK41187QF?p=38&spm_id_from=pageDriver&vd_source=67471d3a1b4f517b7a7964093e62f7e6
一、最小生成树
树的一些概念
-
连通图(Connected Graph):指无向图中任意两个顶点之间都有路径相连。也就是说,在一个连通图中,任意两个顶点之间都可以通过图中的边相连而相互到达。
-
树(Tree):一种无向无环连通图,它由 n n n 个节点和 n − 1 n-1 n−1 条边组成。树具有很多重要的性质,比如任意两个节点之间都有唯一的路径、树中任意两个节点之间的路径长度唯一等等。
-
生成树(Spanning Tree):指一个连通图的一棵包含所有顶点的树,它是由原图的所有顶点和边所组成的子图,且这些边构成一个树。一个图,可能有多个生成树。
-
最小生成树(Minimum Spanning Tree,简称MST)是一个连通的无向图的子图,它包含了图中所有的节点,并且边的总权重最小。
关键特性
- 连通性:最小生成树包含图中所有的节点,并且节点之间是连通的。
- 无环性:最小生成树中没有环(环:从一点出发经过若干条边又回到该点的路径)。
- 边数:对于一个包含 ( V ) 个节点的图,最小生成树包含 ( V-1 ) 条边。
- 最小权重:在所有可能的生成树中,最小生成树的边的权重之和最小。
最小生成树和最短路径的主要区别
- 最小生成树(MST):在一个无向连通图中,找到一棵树,使得它包含图中的所有节点,并且边的总权重最小。MST是关于图的全局结构的优化问题。
- 最短路径(Shortest Path):在图中寻找两个节点之间的路径,使得路径上的边的总权重最小。最短路径是关于图的局部结构的优化问题。
- MST:目的是连接所有节点,形成一个无环的子图,并使得边的总权重最小。
- 最短路径:目的是找到特定两个节点之间的权重最小的路径。
常用算法
有两种主要的算法用于寻找最小生成树:Kruskal算法和Prim算法。
1. Kruskal算法(适合点多边少的图)
Kruskal算法是一种贪心算法,它通过逐步选择权重最小的边,并确保不形成环,最终构建出最小生成树。
步骤:
-
把图 G G G 中所有边全部去掉,得到所有单独的顶点 V V V 构成的图 T T T
-
从图 G G G 中取出当前权值最小的边,如果该边加入 T T T 的边集合后 T T T 不形成回路,则加入 T T T,否则舍弃
-
重复第二步,直到 T T T 中有 n − 1 n-1 n−1 条边( n n n 是定点数)
第二步若遇到两条权值相同的最小权值边,任选一条即可,所以最小生成树可能不唯一
2. Prim算法(适合边多点少的图)
Prim算法也是一种贪心算法,它以一个节点为起点,逐步扩展生成树,每次选择与生成树相连的最小权重边。
步骤:
-
设一空图 U U U,首先将图 G G G 中任意一顶点取出加入 U U U 中
-
从图 U U U 外并与图 U U U 连接的边中,找到权值最小的边和该边连接的顶点并入图 U U U 中
-
重复第二步,直到 U U U 中包含了所有顶点
第二步若遇到两条权值相同的最小权值边,任选一条即可,所以最小生成树可能不唯一
二、示例
- A国有六个城市之间一直没有通信线路,现在国家想使这六个城市通信连通
- 六个城市,相互之间能够建设通信线缆的线路路径距离已测
- 要求以最小的成本建设通信线路,使得这六个城市之间能够互相通信
- 即从任意顶点出发,都可以到达其他顶点,且线缆总长度最小
根据要求:
- 从任意顶点出发,都可以到达其他顶点
- 线缆总长度最小
判断出该示例应使用最小生成树算法
三、代码实现----Matlab
clc,clear
% matlab中,不存在的边设置成0
% 6个顶点,初始化定义6x6的全零矩阵作为邻接矩阵
a = zeros(6);
% 注意,最小生成树是针对无向图的,每条边权重只需要设一次。1到2和2到1是同一条边
% 因此,可仅使用邻接矩阵的上三角矩阵来构造图G
a(1,[2 3])=[14 18]; % 顶点1到其他顶点的边的权重
a(2,[3:5])=[13 18 16]; % 顶点2到顶点3、顶点9的边的权重
a(3,[4 5])=[12 16]; % 同上。因为写过1到3,和2到3的边的权重,无需重复设
a(4,[5 6])=[14 19];
a(5,6)=10;
s=cellstr(strcat('城市',int2str([1:6]')));
G=graph(a,s,'upper'); % 仅使用 A 的上三角矩阵来构造图G。
p=plot(G,'EdgeLabel',G.Edges.Weight); % 绘制出图G
% minspantree函数求解最小生成树
% T=minspantree(G)是默认使用Prim算法
T=minspantree(G,'Method','sparse'); % 可指定使用Kruskal算法
L = sum(T.Edges.Weight) % 对最小生成树的边的权重求和
highlight(p,T,"EdgeColor",'r','LineWidth',2.5)
运行结果:
四、代码实现----python
1. Kruskal算法
(1)表示出示例图
graph = {
'1': {'2': 14, '3': 18},
'2': {'1': 14, '3': 13, '4': 18, '5': 16},
'3': {'1': 18, '2': 13, '4': 12, '5': 16},
'4': {'2': 18, '3': 12, '5': 14, '6': 19},
'5': {'2': 16, '3': 16, '4': 14, '6': 10},
'6': {'4': 19, '5': 10}
}
(2)初始化节点
parent = {vertex: vertex for vertex in graph.keys()}
(3)获取边的两个节点和边的权重
# 获取所有的边
edges = []
for vertex, neighbors in graph.items():
for neighbor, weight in neighbors.items():
edges.append((weight,vertex, neighbor))
# 将 edges 转换为堆
heapq.heapify(edges)
(4)定义并查集函数
需要使用并查集判断一条边加入后是否形成环
并查集的相关概念见:https://blog.csdn.net/m0_65032457/article/details/141224025?spm=1001.2014.3001.5501
def find(parent, vertex):
if parent[vertex] != vertex:
parent[vertex] = find(parent, parent[vertex]) # 路径压缩
return parent[vertex]
def union(parent, vertex1, vertex2):
# 查找两个节点的根
root1 = find(parent, vertex1)
root2 = find(parent, vertex2)
if root1 != root2:
parent[root2] = root1
(5)使用 heapq.heappop()
函数弹出最短边
若加入该边不形成环,则使用 heapq.heappush()
函数将该边加入最小生成树。
堆 heapq
的概念见:https://blog.csdn.net/m0_65032457/article/details/141135370?spm=1001.2014.3001.5501
while edges:
weight, note1, note2 = heapq.heappop(edges)
if find(parent, note1) != find(parent, note2):
union(parent, note1, note2)
heapq.heappush(res, (weight, note1, note2))
Kruskal算法的完整代码
import heapq
def find(parent, vertex):
if parent[vertex] != vertex:
parent[vertex] = find(parent, parent[vertex]) # 路径压缩
return parent[vertex]
def union(parent, vertex1, vertex2):
# 查找两个节点的根
root1 = find(parent, vertex1)
root2 = find(parent, vertex2)
if root1 != root2:
parent[root2] = root1
def kruskal_algorithm(graph):
# 初始化并查集
parent = {vertex: vertex for vertex in graph.keys()}
# 获取所有的边
edges = []
# 初始化一个空的堆用来存放最小生成树的结果
res = []
for vertex, neighbors in graph.items():
for neighbor, weight in neighbors.items():
edges.append((weight,vertex, neighbor))
# 将 edges 转换为堆
heapq.heapify(edges)
while edges:
weight, note1, note2 = heapq.heappop(edges)
if find(parent, note1) != find(parent, note2):
union(parent, note1, note2)
heapq.heappush(res, (weight, note1, note2))
return res
graph = {
'1': {'2': 14, '3': 18},
'2': {'1': 14, '3': 13, '4': 18, '5': 16},
'3': {'1': 18, '2': 13, '4': 12, '5': 16},
'4': {'2': 18, '3': 12, '5': 14, '6': 19},
'5': {'2': 16, '3': 16, '4': 14, '6': 10},
'6': {'4': 19, '5': 10}
}
result = kruskal_algorithm(graph)
print(result)
运行结果:
数据分别代表:边的权重、节点1、节点2
2. Prim算法
算法思路:
- 任意选取图中的一个节点为一个独立的树
- 取出除这棵树外,所有与这棵树相连的节点和到这棵树的距离,加入优先队列
- 弹出优先队列中距离这棵树最近的节点,并将其加入这棵树(更新这棵树的范围)
- 重复2、3步骤,直到遍历完所有的节点
代码思路:
-
算法角度:在 Prim 算法中,需要确定 “树” 的范围,以便找到除这棵树外,所有与这棵树相连的节点。
代码角度:创建一个集合
visited = set()
,用来存放已访问的节点(树范围内的节点都存放于集合中),通过节点是否在集合内判断出节点是否在树中。 -
算法角度:找到树范围外,所有与树相连的节点
代码角度:树范围外 ——> 节点不在集合
visited
内,与树相连 ——> 相连的节点在集合visited
内。(通过这两个条件找出了与树相连的节点和到这棵树的距离,生成了优先队列)
完整代码思路:
(1)表示出示例图
graph = {
'1': {'2': 14, '3': 18},
'2': {'1': 14, '3': 13, '4': 18, '5': 16},
'3': {'1': 18, '2': 13, '4': 12, '5': 16},
'4': {'2': 18, '3': 12, '5': 14, '6': 19},
'5': {'2': 16, '3': 16, '4': 14, '6': 10},
'6': {'4': 19, '5': 10}
}
(2)初始化
# 选取 '1' 作为一个独立的树,权重是 0,前驱节点是None
priority_queue = [(0,'1',None)]
# 初始化集合,用来存放已访问的节点
visited = set()
# 存放最小生成树
res = []
(3)生成优先队列
# 更新相邻节点的距离
for neighbor, distance in graph[note1].items():
if neighbor not in visited: # 与节点 1 相邻的节点 1' 是否在树中
for n, d in graph[neighbor].items(): # 取出该相邻结点 1' 的相邻结点 2'
if n in visited: # 相邻结点 2'在树中,代表相邻的节点 1'与树相连
heapq.heappush(priority_queue,(d,neighbor,n)) # 加入优先队列
(4)更新树的范围
# 弹出堆中距离最小的节点
weight, note1, note2 = heapq.heappop(priority_queue)
priority_queue = [] # 清空优先队列
if note2 != None:
visited.add(note2) # 将前驱节点加入已访问中 (更新树的范围)
res.append((weight, note1, note2)) # 更新最小生成树
visited.add(note1) # 将该节点也加入已访问中 (更新树的范围)
Prim算法的完整代码
import heapq
def prim_algorithm(graph):
# 选取 '1' 作为一个独立的树,权重是 0,前驱节点是None
priority_queue = [(0,'1',None)]
# 初始化集合,用来存放已访问的节点
visited = set()
# 存放最小生成树
res = []
while priority_queue:
# 弹出堆中距离最小的节点
weight, note1, note2 = heapq.heappop(priority_queue)
# print("距离最小的节点是:",weight, note1, note2)
priority_queue = [] # 清空优先队列
if note2 != None:
visited.add(note2) # 将前驱节点加入已访问中
res.append((weight, note1, note2))
# print("最小生成树进度:",res)
visited.add(note1) # 将该节点也加入已访问中
# print("已访问的节点:",visited)
# 更新相邻节点的距离
for neighbor, distance in graph[note1].items():
if neighbor not in visited: # 与节点 1 相邻的节点 1' 是否在树中
for n, d in graph[neighbor].items(): # 取出该相邻结点 1' 的相邻结点 2'
if n in visited: # 相邻结点 2'在树中,代表相邻的节点 1'与树相连
heapq.heappush(priority_queue,(d,neighbor,n)) # 加入优先队列
# print("优先队列:",priority_queue)
return res
graph = {
'1': {'2': 14, '3': 18},
'2': {'1': 14, '3': 13, '4': 18, '5': 16},
'3': {'1': 18, '2': 13, '4': 12, '5': 16},
'4': {'2': 18, '3': 12, '5': 14, '6': 19},
'5': {'2': 16, '3': 16, '4': 14, '6': 10},
'6': {'4': 19, '5': 10}
}
result = prim_algorithm(graph)
print(result)
运行结果:
总结
本文介绍了求解最小生成树的两种算法—— Kruskal和Prim,详细介绍了两种算法的求解思路。分别使用matlab和python进行了算法的实现。