Python实现最小生成树–Prim算法和Kruskal算法 前言
最小生成树涉及到在互联网中网游设计者和网络收音机所面临的问题:信息传播问题。其中最简单的解法是由广播源维护一个收听者的列表,将每条信息向每个收听者发送一次,即单播解法。而单播解法的问题也很明显:路由器网络中,有一些路由器会发送相同的信息,给互联网络增加负担,产生额外流量。另一种方法是洪水解法,每条消息只发送一次,给所有的路由器,每个路由器再尽可能的发送给相连的主机或路由器。洪水解法如果没有限制的话,许多路由器和主机将不停的接收到重复的消息,所以洪水解法一般会给每条消息设置TTL值,以此对信息传播进行限制。
而通过分析和研究,信息广播问题的最优解法,依赖于路由器关系图上选取具有最小权重的生成树。沿着最小生成树的路径层次传播,达到每个路由器只需要处理1次消息,同时总费用最小。
解决最小生成树问题常用的有Prim算法 和Kruskal算法 ,二者均基于贪心算法。Prim算法 思想很简单,即每步都沿着最小权重的边向前搜索,找到一条权重最小的可以安全添加的边,将边添加到树中。Kruskal算法 是对所有的权重进行排序,再次体现贪心算法的思想。通过对权重排序,将权重最小的边开始遍历,直至图中的所有节点都连在同一个树上,此时即为最小生成树。
设计 需求分析 使用python
,通过Prim算法
和Kruskal算法
实现图的最小生成树
,输入数据以存放二维数组形式的逗号分隔值文件进行输入,比如txt
文件或者csv
文件,输出时按照Prim算法
和Kruskal算法
分别输出最小生成树的二维数组。同时使用networkx
模块,绘制出相应的有权图。 具体运行逻辑为:
首先输入存放图数据的txt文件路径,然后系统显示菜单:
请选择相应功能模块:
1: 显示原始关系图及数据
2:查看prim算法演示
3:查看kruskal算法演示
4: 更改源文件
0:退出此次运行
正确的输入范围为0-3,并实现菜单中的相应功能,其输出形式以相应的关系图和二维数组的形式进行输出。 当输入内容不符合规范或致使程序运行出现错误时,会有相应提示并重新显示系统菜单。
系统设计 本程序所用数据为存储在逗号分隔值文件中的二维数组,格式为:“首节点,尾节点,权重”。与一般的图结构程序相比,本程序采用的数据格式只取到程序的必要属性,即“首节点,尾节点,权重”,相比于通常的邻接矩阵或稀疏矩阵,三列元素的优势十分明显:易于分析,直观方便,输入便捷,纠错直接。 其中main函数代码如下: 其中明显可以看出:输入文件名称时,是以调用函数的方式运行,显示菜单也是以函数的方式运行,加上结果必要进行的函数调用,这些都是为了节省代码的复用,使逻辑更加清晰。
此外在main
函数中添加try……except
语句,就在确认函数无逻辑错误的情况下,节省了在对各个函数的异常判断,提高了代码的简洁性。
系统实现 解决最小生成树问题常用的有Prim算法和Kruskal算法,二者均基于贪心算法。
Prim算法 Prim算法思想很简单,即每步都沿着最小权重的边向前搜索,找到一条权重最小的可以安全添加的边,将边添加到树中。
如以下图例(如图5):
Step1:首先从图中选取一个顶点,这一步是随机的,本程序中仍帮助用户做出了选择。
Step2:由当前顶点向未选节点进行遍历,找出最小的边,图中选取D作顶点,加入至已选节点,剩下的全部为未选节点,从顶点开始遍历到的边有DA、DB、DE、DF,最小边为DA无异,将A添加至已选节点中,并从未选节点中删去,进入下一步循环。
Step3:遍历到相连的边有DB、DE、DF、AB,最小边为AB,将B添加至已选节点中,并从未选节点中删去,进入下一步循环。
Step4:遍历到相连的边有DE、DF、BC、BE,最小边为DF,将F添加至已选节点中,并从未选节点中删去,进入下一步循环。
Step5:遍历到相连的边有DE、BC、BE、FG,最小边为BE,将E添加至已选节点中,并从未选节点中删去,进入下一步循环。
Step6:遍历到相连的边有BC、EC、FG、EG,最小边为EC,将C添加至已选节点中,并从未选节点中删去,进入下一步循环。
Step7:遍历到相连的边有FG、EG,最小边为EG,将G添加至已选节点中,并从未选节点中删去,此时未选节点列表为空,符合结束条件,程序结束。 使用代码实现时,由于prim算法
是沿着节点去找边,所以以未选取的节点的数量为限制条件进行搜索,在一个最小生成树结构中,如果有某一个节点与其他任一节点都不存在联系,这个数据就是无效数据,当发现不再有这种数据时,也就是得到了一个完整的最小生成树。程序中,我将i
定义为首节点
,j
定义为尾节点
,在已经选取的节点中遍历i,遍历i的同时在预选取节点中遍历j,这样就可以将所有的节点情况一一遍历。而寻找最小的边时,首先通过“if (li[0] == i and li[1] == j) or (li[0] == j and li[1] == i) :
”进行判断,如果i
节点和j
节点相连,就将‘ij
’作为key
,权值作为value
,加入字典edge_weight
中,这里巧妙的运用了字典的key
是一个字符串的不会乱序的特征,通过lambda
表达式进行排序,很容易按照key
的索引和权重大小,找到符合条件的——最短的边,以及与之相对应的ij首位节点。这里已选节点和预选节点都使用了列表的格式,所以只要再依次把符合条件的尾节点从预选节点中remove
掉,再append
至已选节点列表中即可。代码截图如(图13)。
Kruskal算法 Kruskal算法
是对所有的权重进行排序,再次体现贪心算法的思想。通过对权重排序,将权重最小的边开始遍历,直至图中的所有节点都连在同一个树上,此时即为最小生成树。
如以下图例(如图14,同图5): )
Step1:遍历所有边,最小边是AD或CE,排序中除权重外还会以ASCII码排序,选AD为最小边,加入到最小生成树中,A、D添加至已选节点,剩下的为未选节点。
Step2:继续遍历剩下的边,CE为最小边,加入到最小生成树中,C、E添加至已选节点,剩下的为未选节点。
Step3:继续遍历剩下的边,符合条件的最小边依次为:DF、AB、BE,加入到最小生成树中,将B、F添加至已选节点,剩下的为未选节点。
Step4:同上,虽然BC比EG更小,但点BC已经通过E点连通,所以添加第二最小边EG,此时所有节点在同一个树中,程序结束。 使用代码实现时,由于kruskal算法是依次遍历最小边,所以以最小生成树的边与总节点数的关系作为约束条件。一开始程序沿用prim算法
中将未添加节点作为限制条件,但是很快发现逻辑的疏漏:倘若有A
、B
、C
、D
四个节点,按最小边AB
和CD
添加至最小生成树之后,ABCD
四个点并不连通,属于两个树,模型如下图(图19): 经过观察,最小生成树的边和节点的数目关系符合“边数 = 节点数 – 1
”, 所以以最小生成树的边与总节点数的关系作为约束条件,判断下一条最小边的首尾节点与已选节点的关系。这里由于每次涉及到两个节点、重复节点的添加和去除,所以已选节点和未选节点以集合的形式进行运算,解决了添加节点的重复问题,在从预选节点中去除时,也只需选择在全部节点集合中,而不在已选节点集合中的集合,即“candidate_node = candidate - selected_node
”。
判断逻辑如上述,首先选取最小边,判断最小边的首尾节点是否已经被选,如果目标最小边的首尾节点任有一个节点不在已选节点当中,或者都不在已选节点当中,那么可以直接添加该最小边,并将首尾节点去重添加到已选节点集合当中;如果目标最小边的首尾节点均在已选节点当中,并且此时已连接的最小生成树的边数小于已选节点数减一,那么由于首尾节点均在已选节点集合当中,此时只需要将该最小边添加到最小生成树当中即可。代码截图如(图20)。
功能介绍 本程序使用python3.7.3版本进行编程,如有运行报错,请更换python版本为3.7.3或其他适配版本进行测试。
运行程序后会显示系统名称及作者信息。 使用示例如下:
测试数据及代码 测试数据
首节点
尾节点
权重
A
B
2
A
C
3
B
C
1
B
D
1
B
E
4
E
F
1
C
F
5
F
G
1
D
E
1
完整代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 import networkx as nximport matplotlib.pyplot as pltimport sysimport osG = nx.Graph() P = nx.Graph() K = nx.Graph() nodedict={} edgelist=[] labels={} edges={} def read_file (fileroute ): with open (fileroute,"r" ) as f: for line in f: l=line.strip().split("\t" ) l[2 ] = float (l[2 ]) edgelist.append(l) nodedict[l[0 ]]=l[0 ] nodedict[l[1 ]]=l[1 ] for i in sorted (nodedict): labels[i]=nodedict[i] return (nodedict,edgelist,labels) def origin (fileroute ): edgelist=read_file(fileroute)[1 ] labels=read_file(fileroute)[2 ] G.add_weighted_edges_from(edgelist) edge_labels = nx.get_edge_attributes(G,'weight' ) pos = nx.spring_layout(G) nx.draw_networkx_nodes(G, pos, node_color='g' , node_size=500 , alpha=0.8 ) nx.draw_networkx_edges(G, pos, width=1.0 , alpha=0.5 ) nx.draw_networkx_labels(G, pos, labels, font_size=16 ) nx.draw_networkx_edge_labels(G, pos, edge_labels) plt.title("origin" ) plt.show() """ 实现prim算法的函数 """ def prim_tree (fileroute ): nodedict = read_file(fileroute)[0 ] edgelist = read_file(fileroute)[1 ] labels = read_file(fileroute)[2 ] res=[] prim_candidate_node=[] prim_selected_node=[] for key in labels: prim_candidate_node.append(key) prim_selected_node.append(prim_candidate_node[0 ]) prim_candidate_node.remove(prim_selected_node[0 ]) """ 实现prim算法的逻辑代码 """ while len (prim_candidate_node) > 0 : edge_weight={} beginnode='' endnode='' weight=0 for i in prim_selected_node: for j in prim_candidate_node: for li in edgelist: if (li[0 ] == i and li[1 ] == j) or (li[0 ] == j and li[1 ] == i) : edge_weight[i+j]=li[2 ] for key in sorted (edge_weight.items(),key=lambda kv:(kv[1 ], kv[0 ])): endnode=key[0 ][1 ] weight=float (key[1 ]) beginnode=key[0 ][0 ] break res.append([beginnode,endnode,weight]) prim_selected_node.append(endnode) prim_candidate_node.remove(endnode) print (res) P.add_weighted_edges_from(res) edge_labels = nx.get_edge_attributes(P, 'weight' ) pos = nx.spring_layout(P) nx.draw_networkx_nodes(P, pos, node_color='g' , node_size=500 , alpha=0.8 ) nx.draw_networkx_edges(P, pos, width=1.0 , alpha=0.5 , edge_color='r' ) nx.draw_networkx_labels(P, pos, labels, font_size=16 ) nx.draw_networkx_edge_labels(P, pos, edge_labels) plt.title("prim" ) plt.show() """ 实现kruskal算法的函数 """ def kruskal_tree (fileroute ): edgelist = read_file(fileroute)[1 ] labels = read_file(fileroute)[2 ] nodenum = len (labels) candidate = set () selected_node = set () for key in labels: candidate.add(key) candidate_node = set (candidate) edgesorted = sorted (edgelist, key=lambda weight: weight[2 ]) """ 实现kruskal算法的逻辑代码 """ edge_node = {} edgelist_chosed = [] while len (edgelist_chosed) < nodenum - 1 : for li in edgesorted: if li[0 ] not in selected_node or li[1 ] not in selected_node: edgelist_chosed.append(li) selected_node.add(li[0 ]) selected_node.add(li[1 ]) candidate_node = candidate - selected_node elif li[0 ] in selected_node and li[1 ] in selected_node and len (edgelist_chosed) < len (selected_node) - 1 : edgelist_chosed.append(li) else : continue print (edgelist_chosed) K.add_weighted_edges_from(edgelist_chosed) edge_labels = nx.get_edge_attributes(K, 'weight' ) pos = nx.spring_layout(K) nx.draw_networkx_nodes(K, pos, node_color='g' , node_size=500 , alpha=0.8 ) nx.draw_networkx_edges(K, pos, width=1.0 , alpha=0.5 , edge_color='r' ) nx.draw_networkx_labels(K, pos, labels, font_size=16 ) nx.draw_networkx_edge_labels(K, pos, edge_labels) plt.title("kruskal" ) plt.show() def menu (): print (""" 请选择相应功能模块: 1: 生成原始关系图 2:查看prim算法演示 3:查看kruskal算法演示 4: 更改源文件 0:退出此次运行 """ )def filein (): fileroute = '' flag = True while flag: fileroute = input ('请输入正确的图数据的完整路径:' ) if os.path.isfile(fileroute): flag = False return fileroute def main (): print ("----Python实现最小生成树的两种方式----" ) print ("----作者:Mxdon----" ) fileroute=filein() menu() while True : try : chose = input ("您的输入是数字:" ) if chose=="1" : print ("原始有权图已绘制" ) origin(fileroute) menu() elif chose=="2" : print ("prim算法时的最小生成树的图已绘制" ) print ("---prim算法最小生成树边数据如下:---" ) prim_tree(fileroute) menu() elif chose=="3" : print ("kruskal算法时的最小生成树的图已绘制" ) print ("---kruskal算法最小生成树边数据如下:---" ) kruskal_tree(fileroute) menu() elif chose=="4" : fileroute = filein() menu() elif chose=="0" : print ("---再见---" ) sys.exit(0 ) else : print ("无此选项" ) menu() except Exception as e: print ("\033[31m请检查文件内容是否符合“节点-节点-权重”的格式!\033[0m" ) main()
参考文章 数据结构(十):最小生成树 python最小生成树kruskal与prim算法 最小生成树的两种方法(Kruskal算法和Prim算法) 最小生成树-Prim算法和Kruskal算法 数据结构基础概念篇 数据结构:八大数据结构分类 数据结构与算法Python版-陈斌 NetworkX系列教程(10)-算法之二:最小/大生成树问题 Python 3 教程