1 实现概述
实验实现了Prim(基于堆)和Kruskal(基于UNION-FIND)算法。堆和并查集结构由自己定义。图的定义使用库networkx,能方便的为节点和边赋予信息。并且结合networkx和matplotlib将图可视化。在测试上使用随机数通过节点,边和权重的随机生成进行验证,计算算法运行时间进行比对。
在堆的结构上,内部使用三个数组分别记录节点,对应节点的最小权重和邻接点。每次在上下调整的时候同时调整三者。在更新值的时候通过节点的列表找到数组中的位置,然后视情况上下调整而非重新建堆。
在并查集的结构上,采用字典的数据结构key为head,value为属于head的所有节点。查询时候只需在value中查找,合并将value合并并删除对应元素较少的key即可。
2 效果演示
输入图:
执行结果:
3 堆和并查集类定义
类名:Heap
类属性:
-
self.heap = list() 记录堆中的值 对应边的权重
-
self.node = list() 记录堆中权重对应的起始节点
-
self.neighbor = list() 记录堆中节点的邻接节点
类方法:
类方法 | 作用 |
---|---|
def _init_(self): | 初始化类 |
def _str_(self): | 返回类的信息 |
def Up(self, i: int): | 将指定节点向上调整 |
def Down(self, i: int): | 将指定节点向下调整 |
def Insert(self, weight: int, node: str, neighbor: str): | 拆入新节点 |
def GetMin(self): | 获取堆的最小值 |
def Change(self, weight: int, node: str, neighbor: str): | 更改指定节点的值 |
类名:Union_Find
类属性:
-
self.data = dict() 采用字典的方式,key为head value为属于head的所有点
类方法:
类方法 | 作用 |
---|---|
def _init_(self): | 初始化类 |
def _str_(self): | 返回类的信息 |
def Find(self, node: str): | 放回节点对应的head |
def Union(self, head1: str, head2: str): | 将两个head合并 |
4 其他函数定义
函数 | 作用 |
---|---|
def Prim(g: nx.Graph, begin: str) -> nx.Graph: | 使用prim算法将给定图转化成最小生成树 |
def Kruskal(g: nx.Graph) -> nx.Graph: | 使用kruskal算法将给定图转化成最小生成树 |
def show(g: nx.Graph, name: str): | 调用networkx和matplotlib库打印输入图 |
def Test(test_times: int, max_nodes: int, max_weights): | 运行时间测试,参数分别制定测试次数,最大节点数目和最大权重值。 |
5 测试集
在这部分中,通过指定测试集数量,最大节点数和最大权重数目,我们使用random模块随机生成节点,边和权重,通过networkx库的内置函数is_connected检验随机生存的图是否联通,若是则放入生产树算法,最后分别统计运行时间,我们以30000次测试,50为最大权重。分别测试10、20、50、100、200节点数目下的运行时间。如下图所示输出:
统计如下表所示:
时间/节点数 | 10 | 20 | 40 | 100 | 200 |
---|---|---|---|---|---|
Prim时间 | 1.317 | 4.922 | 22.485 | 78.927 | 332.968 |
Kru时间 | 1.114 | 4.012 | 24.599 | 124.029 | 668.504 |
Prim/Kru | 1.182226212 | 1.226819541 | 0.914061547 | 0.636359239 | 0.498079293 |
可见在节点数较少的时候,kru算法有较好的运行效果,但是当节点增加到100以上的时候,kru算法的性能急剧下降,在200节点的时候性能只有prim算法的1/2左右。
接着我们探讨在节点数一定,运行效率相似的情况下,稀疏图或者是稠密图对于算法的影响,故而,我们选取30000次和30~40节点作为条件,定义稀疏图的边数在 [ n − 1 , n 2 / 4 ] [n-1,n^2/4] [n−1,n2/4]之间,稠密图的边数在 [ n 2 / 4 , n 2 / 2 ] [n^2/4,n^2/2] [n2/4,n2/2]之间,比较结果。
时间/类型 | 40节点稀疏图 | 40节点稠密图 | 上升比例 |
---|---|---|---|
Prim时间 | 37.597 | 69.802 | 0.856584302 |
Kru时间 | 37.57 | 87.027 | 1.316396061 |
可见对于稠密图而言,kru算法运行时间相对与prim算法提高幅度更大。
综上所属,kru算法适合较少节点数和稀疏图,在稠密以及较多节点数的时候表现急剧下降,但是kru算法较为简洁,并且有更低的空间复杂度。
6 遇到的疑惑
首先是选择怎样的存储结构的问题,之前的proj1逻辑上使用了邻接表,实际上借助字典的数据结构实现。但如果为图增加权重信息,又会重新设计,并且也不便于以后项目的进行。
所以选择了现成的网络图分析的库networkx,省去了考虑图的构建,能更容易的关注于算法本身。
两种算法的思路都容易掌握,问题是如何构建堆和并查集,其中重点又是堆的构建。我们没有使用库heapq,因为考虑到更新的时候需要删除整个堆重新构建。问题是如何在记录权重的同时记录所属节点和对应临界点。
我们考虑了两种方法,[(node,weight,next),…]的结构和三个列表的结构。我们虽然使用了第二种,但是现在看来应当是第一种的逻辑更为合理并且操作也更加方便。
仍然是堆的建立,应当将位置0空出,从1开始记录数据更为方便。可见在堆上仍有很大的改进空间。
7 源代码
import networkx as nx
import matplotlib.pyplot as plt
import matplotlib as mpl
import time
import random
"""
缺少包请cmd
pip install networkx
pip install matplotlib
如果遇到图片打开后就停止运行的 很抱歉是编辑器的问题 pycharm专业版可以 社区版不行
很抱歉实在调不过来了 把窗口关掉就行了
友情提示 虽然算法跑的快 但是networkx库跑的慢 增加边需要很长时间
测试量和节点不要太大 具体结果看报告就好
"""
class Heap():
# 函数中的索引按照123开始
def __init__(self):
self.heap = list()
self.node = list()
self.neighbor = list()
def __str__(self):
ans = '堆中元素:' + self.heap.__str__() + '\n' + '对应节点:' + self.node.__str__() + '\n' + '最小邻居节点:' + self.neighbor.__str__()
return ans
def Up(self, i: int):
while True:
if i < 2:
return
if self.heap[i // 2 - 1] > self.heap[i - 1]:
self.heap[i // 2 - 1], self.heap[i - 1] = self.heap[i - 1], self.heap[i // 2 - 1]
self.node[i // 2 - 1], self.node[i - 1] = self.node[i - 1], self.node[i // 2 - 1]
self.neighbor[i // 2 - 1], self.neighbor[i - 1] = self.neighbor[i - 1], self.neighbor[i // 2 - 1]
i = i // 2
else:
return
def Down(self, i: int):
n = len(self.heap)
while True:
if i > n // 2:
return
temp = 2 * i + 1 if 2 * i + 1 - 1 <= n - 1 and self.heap[2 * i - 1] > self.heap[(2 * i + 1) - 1] else 2 * i
if self.heap[temp - 1] < self.heap[i - 1]:
self.heap[temp - 1], self.heap[i - 1] = self.heap[i - 1], self.heap[temp - 1]
self.node[temp - 1], self.node[i - 1] = self.node[i - 1], self.node[temp - 1]
self.neighbor[temp - 1], self.neighbor[i - 1] = self.neighbor[i - 1], self.neighbor[temp - 1]
i = temp
else:
return
def Insert(self, weight: int, node: str, neighbor: str):
self.heap.append(weight)
self.node.append(node)
self.neighbor.append(neighbor)
# 向上调整
self.Up(len(self.heap))
def GetMin(self):
ans = {'weight': self.heap[0], 'node': self.node[0], 'neighbor': self.neighbor[0]}
self.heap[0] = self.heap[-1]
self.node[0] = self.node[-1]
self.neighbor[0] = self.neighbor[-1]
self.heap.pop()
self.node.pop()
self.neighbor.pop()
self.Down(1)
return ans
def Change(self, weight: int, node: str, neighbor: str):
i = self.node.index(node)
self.heap[i] = weight
self.neighbor[i] = neighbor
if (i + 1) // 2 - 1 >= 0 and self.heap[i] < self.heap[(i + 1) // 2 - 1]:
# 变更后的值 小于父节点的值
# 向上调整
self.Up(i + 1)
else:
self.Down(i + 1)
class Union_Find():
def __init__(self, node: list):
# 采用字典的方式 key为head value为集合
self.data = dict()
for i in node:
self.data.update({i: [i]})
def __str__(self):
return self.data.__str__()
def Find(self, node: str):
# 返回节点的父节点
for key, value in self.data.items():
# print(key, value)
if node in value:
return key
else:
return None
def Union(self, head1: str, head2: str):
if len(self.data[head1]) < len(self.data[head2]):
left = head2
temp = head1
else:
left = head1
temp = head2
# 将较少的集合扩展到较多的集合
self.data[left].extend(self.data[temp])
# 删除短集合
del self.data[temp]
def show(Ori: nx.Graph, Prim: nx.Graph, Kruskal: nx.Graph):
lables = ['Ori', 'Prim', 'Kruskal']
for i in range(3):
plt.subplots(1, 1)
# plt.figure(num=lables[i])
g = eval(lables[i])
Edges = []
colors = [g.edges[u, i]['weight'] for u, i in g.edges]
weight_sum = sum(colors)
nx.draw_networkx(g, pos=nx.circular_layout(g), edge_color=colors,
width=5, edge_cmap=plt.cm.Blues, with_labels=True, edge_vmin=0, alpha=0.9, node_size=400)
pc = mpl.collections.PatchCollection(Edges, cmap=plt.cm.Blues)
pc.set_array(colors)
plt.colorbar(pc, ax=plt.gca())
title = f'{lables[i]}——Sum(weight)={weight_sum}'
plt.title(title)
plt.show()
plt.close('all')
def Prim(g: nx.Graph, begin: str) -> nx.Graph:
h = Heap()
tree = nx.Graph()
h.Insert(0, begin, None)
for item in set(g.nodes()) - {begin}:
h.Insert(float('inf'), item, None)
while len(h.heap) != 0:
temp = h.GetMin()
node = temp['node']
neighbor = temp['neighbor']
weight = temp['weight']
if neighbor is not None:
tree.add_edge(node, neighbor, weight=weight)
# 新邻居中的最小值
for now_neighbor in g.neighbors(node):
now_weight = g.get_edge_data(now_neighbor, node)['weight']
# 有些节点已经在堆中去除了 为了不写复杂的判断语句 直接使用try-except
try:
i = h.node.index(now_neighbor)
if h.heap[i] > now_weight:
h.Change(now_weight, now_neighbor, node)
except:
pass
return tree
def Kruskal(g: nx.Graph) -> nx.Graph:
edge_lst = []
for u, i in g.edges:
edge_lst.append((u, i, g.edges[u, i]['weight']))
# 将边集合按照升序排列
edge_lst = sorted(edge_lst, key=lambda edge: edge[2])
# 初始化union
un = Union_Find(list(g.nodes))
# 初始化生成树
tree = nx.Graph()
for item in edge_lst:
begin = item[0]
end = item[1]
weight = item[2]
head1 = un.Find(begin)
head2 = un.Find(end)
if un.Find(begin) != un.Find(end):
tree.add_edge(begin, end, weight=weight)
un.Union(head1, head2)
return tree
def Test(test_times: int, max_nodes: int, max_weights):
print(f'测试集个数:{test_times}\t最大节点个数:{max_nodes}\t最大权重值:{max_weights}\t')
real_test = 0
real_nodes = 0
real_vexs = 0
time_prim = 0
time_kru = 0
finish = 1
for _ in range(test_times):
print(f'\r' + '=' * int(50 * finish / test_times) + f'=>{finish}/{test_times}', end='')
finish += 1
# 定义一个无向图
g = nx.Graph()
# 随机生成节点个数
now_nodes = random.randint(30, max_nodes)
# 随机生成边的个数 最少为n-1 最多为n(n-1)/2 尽可能保持联通 可以有重边
# now_nodes * (now_nodes - 1) / 2
now_vexs = random.randint(now_nodes, int(now_nodes * (now_nodes - 1) / 2))
# 加入边
for _ in range(now_vexs):
# 随机生成起始节点和边及其权重
begin = str(random.randint(0, now_nodes))
end = str(random.randint(0, now_nodes))
weight = random.randint(0, max_weights)
g.add_edge(begin, end, weight=weight)
if nx.is_connected(g):
real_test += 1
real_nodes += now_nodes
real_vexs += now_vexs
t1 = time.time()
Prim(g, list(g.nodes)[0])
t2 = time.time()
Kruskal(g)
t3 = time.time()
time_prim += t2 - t1
time_kru += t3 - t2
print(f'\n有效测试数量{real_test}\n总节点数{real_nodes}\n总边数{real_vexs}\nPrim用时{time_prim}\tKru用时{time_kru}')
if __name__ == '__main__':
while True:
print('如果遇到图片打开后就停止运行的 很抱歉是编辑器的问题 实在调不过来了 把窗口关掉就行了')
print('1\t示例执行')
print('2\t自定义图执行')
print('3\t测试函数')
print('0\t退出')
choose = int(input('请输入要执行的选项:'))
if choose == 1:
g = nx.Graph()
g.add_weighted_edges_from(
[('A', 'B', 6), ('A', 'C', 1), ('A', 'D', 5), ('B', 'E', 3), ('B', 'C', 5), ('C', 'E', 6),
('C', 'F', 4),
('C', 'D', 5), ('D', 'F', 2), ('E', 'F', 6)])
tree1 = Prim(g, 'A')
tree2 = Kruskal(g)
show(g, tree1, tree2)
elif choose == 2:
print('请按照以下格式输入边')
print('A B 9;A C 3')
print('回车结束输入')
g = nx.Graph()
try:
temp = input()
temp = temp.split(';')
for item in temp:
a = item.split(' ')
g.add_edge(a[0], a[1], weight=int(a[2]))
except:
pass
print('输入结束')
if nx.is_connected(g):
tree1 = Prim(g, 'A')
tree2 = Kruskal(g)
show(g, tree1, tree2)
else:
print('请您输入连通图……')
elif choose == 3:
print('友情提示 虽然算法跑的快 但是networkx库跑的慢 增加边需要很长时间\n测试量和节点不要太大\n测试结果放在报告了')
test_times = int(input('测试集个数'))
max_nodes = int(input('最大节点个数 n>2'))
max_weights = int(input('最大权重值'))
Test(test_times, max_nodes, max_weights)
elif choose == 0:
break
else:
print('您输错了……')