数据结构大作业-算法可视化设计
中国邮递员可视化设计
中国邮递员问题背景
中国邮递员问题是邮递员在某一地区的信件投递路程问题。邮递员每天从邮局出发,走遍该地区所有街道再返回邮局,问题是他应如何安排送信的路线可以使所走的总路程最短。这个问题由中国学者管梅谷在1960年首先提出,并给出了解法——“奇偶点图上作业法”,被国际上统称为“中国邮递员问题”。用图论的语言描述,给定一个连通图G,每边e有非负权),要求一条回路经过每条边至少一次,且满足总权最小。
成品展示及功能介绍
左边一列有三个按钮,它们的功能依次是 初始化地图、下一步、上一步。
右边的大画布就是 地图的显示界面,地图的生成是根据 用户在算法程序中输入的地图边信息构成的,地图中的每个结点的 相对位置是固定的 ,但是 **绝对位置是随机的 **,用户可以多按几次初始化地图按钮,生成他们想要的地图样式。
每当按下一步按钮,邮递员就会依照算法程序得到的最终路径来前进一步,上一步按钮顾名思义就是返回上一次邮递员的位置。
邮递员的前进是通过结点和边的颜色变化实现的,邮局(起点)为浅绿色、未走过的结点为浅蓝色、未走过的边为黑色,走过一次的结点和边都为金色,走过多次的结点和边都为紫色,经过邮局其颜色会变为红色,到达终点(回到邮局)其颜色又会变为浅绿色,边的权值为黑色方框中的白色字体。
走了一半:
到达终点:
不同的地图:
设计环境
python 3.9
使用的库主要有:
PyQt5、matplotlib、networkx、read_csv
设计思路
用 read_csv库 读取由 C#编写的算法程序生成的保存有 中国邮递员街区里的边的信息 和 最短路径所经过的结点信息 的 csv文件,用 network库 来构建无向图即 生成街区地图,邮递员每走过一条街道,这条 街道和其末端结点的颜色 就会被更改,以此来区分经过的结点和未经过的结点,并且每走一步会 判断这条街道和其末端结点是否被走过 ,被走过多次的街道和结点会用其他的颜色来标记它,用 matolotlib库 将街区地图画在画布上,最后将 承载街区地图的画布安置在 PyQt5 设计的 UI界面 上,并且添加 按钮控制 街区地图的生成、上一步、下一步。
具体实现
读取csv文件
代码实现还是很简单的,网上看一两个例子就能模仿出来,没啥好说的
import csv |
network构造无向图 (重点)
初始化
定义一个初始的无向图,然后给它添加边(边的信息是读取上面的csv得到的),边的属性包括它的权值 - weight,起始结点和末端结点,颜色 - color(初始化时所有的边默认为黑色,表示未走过)
import networkx as nx |
添加属性
为了让之后结点颜色发生变化,我们要给结点添加一个 color 属性,并且我们之后还得利用 nx.layout.spring_layout 函数来生成结点的坐标,这个函数是通过 Fruchterman-Reingold 力导向算法 来计算结点位置的,简单的说就是,边的吸引力属性值越大,这条边两端的结点离得越近,即边越短,所以我们通过将边的 weight(权值) 取倒数的方法构建 边的吸引力属性,0.5是由测试得到的效果较好的值。
#添加结点的颜色属性 |
获取结点的坐标
这个布局函数是我们构建图的关键,我们需要了解它的参数
self.pos = nx.spring_layout(self.G, weight='attraction', iterations=pow(node_num, 2)) |
官方文档:
这里我只介绍几个关键参数:
weight :
它的值必须是边的属性,布局函数会依照它的值调用 Fruchterman-Reingold 力导向算法 定位节点生成所有点的坐标,我们填入的属性可以被称为 边两端结点吸引力,即一个边的该属性值越大,它两端的结点离得越近,边越短。所以我之前在给边添加属性的时候,添加了一个 ‘attraction’ 属性,它就是用于此处,它的值是边权值属性的倒数,即边的权值越大,边两端结点的吸引力越小,边越长。
力引导布局(Force-directed Layout)
力引导布局最早由Peter Eades在1984年的“启发式画图算法”一文中提出,目的是减少布局中边的交叉,尽量保持边的长度一致。此方法借用弹簧模型模拟布局过程:用弹簧模拟两个点之间的关系,受到弹力的作用后,过近的点会被弹开而过远的点被拉近;通过不断的迭代,整个布局达到动态平衡,趋于稳定。其后,“力引导”的概念被提出,演化成 力引导布局算法FR(Fruchterman-Reingold算法)——丰富两点之间的物理模型,加入点之间的静电力,通过计算系统的总能量并使得能量最小化,从而达到布局的目的。这种改进的能量模型,可看成弹簧模型的一般化。
无论是弹簧模型还是能量模型,其算法的本质是要接一个能量优化问题,区别在于优化函数的组成不同。优化对象包括引力和斥力部分,不同算法对引力和斥力的表达方式不同。
itrations:
它的值是一个int类型数值,表示 Fruchterman-Reingold算法的 最大迭代次数,理论上这个值越大,图上点的分布越符合预期,但是过大的值又会影响运行效率。我给它设定的值是 图中结点个数的平方。
seed
它的值是一个int类型数值,默认为None,Fruchterman-Reingold算法会根据它的值生成不同的结点布局,如果这个值被固定了,那么生成的图将是唯一的。为了带有随机性,我没有设置它,保持默认值。
将图画出来
这一步就是设定一些字体大小,结点大小,用 nx.draw 画出所有结点的初始状态,然后再用nx.draw_networkx_nodes 单独画起点结点,再用 nx.draw_networkx_labels 和 nx.draw_networkx_edge_labels 画出结点的标签——结点名称或边的权值,最后将图画在画布上。
# 画出无向图 |
上一步与下一步的控制
这里我定义了一个 全局变量i 来记录邮递员在走最终路径时走到了第几个结点,即i作为最终路径经过的结点列表的下标,按下一步按钮,变量i的值加一;按上一步减一。
邮递员每走一步或者退一步,都要判断当前点是否走过,即利用结点的颜色属性和边的颜色属性,如果满足条件就更新当前结点的颜色属性以及边的颜色属性,然后将其画到画布上。
这里就是需要注意一下获取具体某一条边的颜色属性时,nx.get_edge_attributes(self.G, ‘color’)[(visited_node[m - 1], visited_node[m])] 后面 [] 里的值填入当前边起始结点和末端结点组成的元组时,有时候会找不到这条边的属性,发生报错。这是因为无向图的一条边有两种表现形式,但获取边的属性值时只能使用添加边时的那种表现形式,比如边AB,z在无向图中可以写成(‘A’,’B’),也可以写成 (‘B’,’A’),但是添加边时用的是(‘A’,’B’),那个查询边AB属性时,填入(‘B’,’A’),就会报错。所以我这里使用了 try…except…语句。
try: |
PyQT5设计UI
很常见的设计,这里就直接贴代码了,下面有一个 eval()函数还是挺有意思的
NumButtons = ['init_graph', 'next_step', 'previous_step'] |
UI美化
这里仅仅简单地调用了别人写好的皮肤,直接加载到UI上,有了它UI高大上了好多
from qt_material import apply_stylesheet |
总结
这次数据结构大作业我是负责UI设计部分的,一共花了5天时间,中间摸鱼了好一段时间,写的时间还是挺长的。一开始对实现这种图的可视化还是一概不知的,到处找资料,找了一天了解到了python的networkx库专门用于构建和操作复杂的图结构,然后就想去b站找视频学习一下,但是b站关于networkx的视频几乎没有,索性上油管上看着机翻字幕学了一会,再加上看各个博客关于networkx用法,自己再复制别人的代码调调,感觉有点认识后,直接照着官方文档写了两天,写出了充满bug的demo,最后就不断优化不断删删改改。
总的来说,这几天的生活还是很充实的,学到了挺多东西的,不仅挑战了自己,锻炼了python基本功,还见识了很多有意思的东西。但是也有一些遗憾,有几个想实现的东西没有实现,有一些想尝试的东西没有时间去尝试,希望下次再遇到这种做项目的挑战的时候,自己能做的更好。