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