一、prim算法精讲
题目连接:53. 寻宝(第七期模拟笔试) (kamacoder.com)
文章讲解:代码随想录 (programmercarl.com)——最小生成树之prim
最小生成树:所有节点的最小连通子图,即以最小的边的权重将图中所有的点连接起来。(花最小的成本连通所有的点)
最小生成树任务:如果图中有 n 个节点,那么一定可以用 n - 1 条边将所有节点连接起来,最小生成树就是找到这 n - 1 条边。
prim算法定义:从节点的角度,采用贪心的策略,每次寻找距离最小生成树最近的节点,并加入到最小生成树中。
prim三部曲:
step1. 选距离生成树最近的节点
step2. 最近节点加入生成树
sept3. 更新非生成树节点到生成树的最短距离(即更新minDist数组)
minDist数组:用来记录每一个节点距离最小生成树的最近距离。
# v -> 节点数量
# e -> 边数量
# edges -> 存储边信息的列表,每一个边是一个元组(x, y, k),表示从节点 x 到节点 y 的权重为 k
def prim(v,e, edges):
# 初始化邻接矩阵,因为要求最小值,所以设置为正无穷
grid = [[float('inf')] * (v + 1) for _ in range(v + 1)]
# 填充邻接矩阵
for edge in edges:
x, y, k = edge
grid[x][y] = k # 从 x 到 y 的权重
grid[y][x] = k # 从 y 到 x 的权重
# 初始化 minDist 数组,用来记录当前节点到最小生成树的最短距离
minDist = [float('inf')] * (v + 1)
# 从1节点开始,到自己的距离为 0
minDist[1] = 0
# 记录节点是否在树种
isInTree = [False] * (v + 1)
# prim主循环,要循环 (v - 1) 次
for i in range(1, v):
# step1. 找到距离生成树最近的节点
cur = -1 # 初始化,找到当前未在生成树中的离树最近的节点
minVal = float('inf') # 初始化最小值为无穷大
# 遍历从 1 到 v 的所有节点
for j in range(1, v + 1):
# 如果不在树内 且 距离比最小值更小
if not isInTree[j] and minDist[j] < minVal:
# 更新最小值为当前值,且更新当前节点
minVal = minDist[j]
cur = j
# step2. 将当前选中的最近节点加入生成树
isInTree[cur] = True
# step3. 更新非生成树节点到生成树的最小距离
for j in range(1, v + 1):
if not isInTree[j] and grid[cur][j] < minDist[j]:
minDist[j] = grid[cur][j]
# 统计最小生成树的权重和
result = sum(minDist[2:v+1])
return result
if __name__ == '__main__':
v, e = map(int,input().split())
edges = []
# 遍历每一条边
for _ in range(e):
x, y, k = map(int, input().split())
edges.append((x, y, k))
print(prim(v, e, edges))
"""
扩展,需要输出边
"""
def prim(v,e, edges):
grid = [[float('inf')] * (v + 1) for _ in range(v + 1)]
for edge in edges:
x, y, k = edge
grid[x][y] = k
grid[y][x] = k
minDist = [float('inf')] * (v + 1)
minDist[1] = 0
isInTree = [False] * (v + 1)
# 改动,增加数组记录边
parent = [-1] * (v + 1)
for i in range(1, v):
cur = -1
minVal = float('inf')
for j in range(1, v + 1):
if not isInTree[j] and minDist[j] < minVal:
minVal = minDist[j]
cur = j
isInTree[cur] = True
for j in range(1, v + 1):
if not isInTree[j] and grid[cur][j] < minDist[j]:
minDist[j] = grid[cur][j]
# 改动,记录边,注意数组指向的顺序 j -> cur
parent[j] = cur
# 改动,输出边
for i in range(1, v + 1):
print(f"{i} -> {parent[i]}")
if __name__ == '__main__':
v, e = map(int,input().split())
edges = []
for _ in range(e):
x, y, k = map(int, input().split())
edges.append((x, y, k))
print(prim(v, e, edges))
二、kruskal算法精讲
题目连接:53. 寻宝(第七期模拟笔试) (kamacoder.com)
文章讲解:代码随想录 (programmercarl.com)——最小生成树之kruskal
kruskal思路:
· 先按边的权值进行排序;
· 再遍历排序后的边
如果边首位两个节点在同一个集合,说明会形成环。
如果边首尾两个节点不在同一个集合,加入最小生成树,并把两个节点加入同一集合。
Note:判断是否在同一集合,用到并查集的内容。
class UnionFindSet:
def __init__(self, n):
self.parent = list(range(n))
def find(self, u):
if self.parent[u] != u:
self.parent[u] = self.find(self.parent[u])
return self.parent[u]
def isSame(self, u, v):
u = self.find(u)
v = self.find(v)
return u == v
def join(self, u, v):
root_u = self.find(u)
root_v = self.find(v)
if root_u != root_v:
self.parent[root_v] = root_u
def kruskal(v, edges):
# step1. 按边的权重大小,从小到大重新排序
# edge 是一个(v1, v2, val)的元组,v1 和 v2 分别是边的两个端点,val 是边的权重
edges.sort(key=lambda edge: edge[2])
# step2. 初始化并查集,编号从 1 到 v
ufs = UnionFindSet(v + 1)
# 记录最终权重总和
result = 0
# step3. 遍历所有边,用并查集查找是否属于同一集合
for edge in edges:
v1, v2, val = edge # 解包元组,获取边的两个端点和权值
x = ufs.find(v1) # 查找边的一个端点的根节点
y = ufs.find(v2) # 查找边的另一个端点的根节点
# 如果两个根节点不同,说明不在一个集合内
if x != y:
result += val # 边加入生成树
ufs.join(x, y) # 合并两个节点
return result
if __name__ == '__main__':
v, e = map(int, input().split())
edges = []
# 读取每条边的信息
for _ in range(e):
v1, v2, val = map(int, input().split())
edges.append((v1, v2, val))
result = kruskal(v, edges)
print(result)
"""
记录边
"""
class UnionFindSet:
def __init__(self, n):
self.parent = list(range(n))
def find(self, u):
if self.parent[u] != u:
self.parent[u] = self.find(self.parent[u])
return self.parent[u]
def isSame(self, u, v):
u = self.find(u)
v = self.find(v)
return u == v
def join(self, u, v):
root_u = self.find(u)
root_v = self.find(v)
if root_u != root_v:
self.parent[root_v] = root_u
def kruskal(v, edges):
edges.sort(key=lambda edge: edge[2])
ufs = UnionFindSet(v + 1)
result_val = 0 # 记录最小生成树的值
resule_edge = [] # 记录最小生成树的边
for edge in edges:
v1, v2, val = edge
x = ufs.find(v1)
y = ufs.find(v2)
if x != y:
result_val += val
ufs.join(x, y)
resule_edge.append(edge) # 保存最小生成树的边
return result_val, resule_edge
if __name__ == '__main__':
v, e = map(int, input().split())
edges = []
for _ in range(e):
v1, v2, val = map(int, input().split())
edges.append((v1, v2, val))
result_val, resule_edge = kruskal(v, edges)
print(result_val)
for edge in resule_edge:
print(f"{edge[0]} - {edge[1]} : {edge[2]}")
总结
区别:prim 算法是维护点的集合,kruskal 算法是维护边的集合
选择:
如果一个图节点多,但是边相对少,选择 kruskal 算法更优;
若果一个图边多,节点少,选择 prim 算法更优。