title: Prime&Kruskal
date: 2023-11-18 14:42:49
tags: algorithm
最小生成树
描述
生成树:设G = (V,E)是一个无向连通图,如果G的生成子图T = (V,E‘)是一棵树,则T就是G的一棵生成树。包含图的所有顶点和部分边
最小生成树:图的权值和最小的一棵生成树。
贪心策略
选择能使迄今为止所计入的边的成本和增加得最小的那条边。
Prime可以理解为一个强盗,根据自己有的结点抢别人的结点和边,但每次只抢自己有的结点和别人的结点连成的边最短的边。
Kruskal可以理解为一个国王,拥有所有的边,将按非降次序将边分配入最小生成树,但是这些边不能构成环。
Prim算法
设集合S是已经选择的边构成的一棵树,下一条将加入S的边(u,v)是U(G的点集)中但不在S中的,且使得SU{(u,v)}的最小成本边。
步骤:
- 初始化变量,设U是G的点集,S是找到的点的集合;
- 随机选择点a,将a加入集合S;
- 寻找下一个加入的点:
- 找到一个顶点在S中,一个顶点在U-S中的最小权值;
- 将最小权值的边在U-S中的点加入S;
- 重复,直到所有点都在S中;
def prim(COST, n):
T = [] # 存放最小生成树的边
mincost = 0 # 最小生成树的总成本
NEAR = [0] * n # NEAR数组
# 选择初始边
k, l = 0, 0
mincost = COST[0][1]
T.append([0, 1])
for i in range(n):
if COST[i][1] < COST[i][0]:
NEAR[i] = 1
else:
NEAR[i] = 0
# 重复直到所有顶点都被访问
for _ in range(2, n):
min_edge = float('inf')
j = 0
for i in range(1, n):
#找到下一个加入S的结点j
#near[i] != 0 表示i有不在S中的邻接结点,然后找到cost最小的边的两个顶点
if NEAR[i] != 0 and COST[i][NEAR[i]] < min_edge:
min_edge = COST[i][NEAR[i]]
j = i
#将节点j和邻接结点j加入S,他们的边加入树
T.append([j, NEAR[j]])
mincost += min_edge
#加入S的标志,下一次遍历整个结点找入树的结点的时候就不会找到j
NEAR[j] = 0
#更新所有结点的near,如果结点k有最小长度的邻接结点near[k]的长大于k到j的长,那么设定j为k的有最小cost的邻近结点
for k in range(n):
if NEAR[k] != 0 and COST[k][NEAR[k]] > COST[k][j]:
NEAR[k] = j
if mincost >= float('inf'):
print('no spanning tree')
else:
return T, mincost
# 示例用法
COST = [
[0, 2, 4, 0],
[2, 0, 1, 3],
[4, 1, 0, 0],
[0, 3, 0, 0]
]
n = 4
T, mincost = prim(COST, n)
print('最小生成树的边:', T)
print('最小生成树的总成本:', mincost)
要点:S加入新结点j之后一定要更新所有结点的最小长度的邻接结点的状态,如果满足不在S中且原本的最小长度的邻接结点的连接边的长度小于到结点j的长度,更新此节点的最小长度的邻接结点的长度为这个结点到j的边的距离。
Kruskal算法
Kruskal假设图中边成本都已知,从所有边集合选出成本最小并且不会和已加入的边构成环的边加入最小生成树的边集,并且加入树的点集合,更新边集合。
步骤:
- 假设图G是一个森林,每个结点时一棵独立的树;
- 将所有的边加入集合S;
- 在S中找到最短的边(u,v),如果u和v不在同一棵树内,那么将u和v两树合并,将(u,v)加入生成树的边集;
- 重复直到点都在同一棵树中。
def kruskal(E,cost,n,T,mincost):
parent = [-1]*n#索引代表顶点,值代表属于的集合,-1代表顶点是独立的树
T = []
mincost = 0
def find(u):
if u == -1:
return u;
else:
return find(parent[u]);
def union(j,k):
parent[j] = k#将j加入k的集合
edge = [];
for u in range(n):
for v in range(n):
if cost[u][v] != float('inf'):
edge.append(cost[u][v])#将所有边加入边集合
edge.sort()#将edge中的边按成本从小到大排序
for cost,u,v in edge:
if(len(T) == n - 1):
break
j = find(u)#查找u属于的集合
k = find(v)#查找k属于的集合
if j != k:
T.append(cost[u][v])
mincost += cost[u][v]
union(u,v)#u和v合并成一个集合
if len(T) != N-1: # 如果最小生成树的边数不为n-1,则打印提示信息
print('no spanning tree')
return T, mincost # 返回最小生成树的边集和成本
这里为了确保选取的边不构成一个环,采用了并查集(Union-Find),即在同一个集合的顶点不能将这两顶点的边加入最小生成树的边集。初始化各个顶点的集合为-1表示独立,利用find找到要加入边集合的边的两顶点是否属于同一集合,如果属于同一集合,表示将构成环(你想想在一棵树里两子节点的父节点相同,这两个节点还有一条边,那么就闭环了);如果不属于一个集合,那么两顶点的边加入边集,将这两个顶点放入同一个集合。