图:无向图(Graph)基本方法及Dijkstra算法的实现 [Python]
一般来讲,实现图的过程中需要有两个自定义的类进行支撑:顶点(Vertex)类,和图(Graph)类。按照这一架构,Vertex类至少需要包含名称(或者某个代号、数据)和邻接顶点两个参数,前者作为顶点的标识,后者形成顶点和顶点相连的边,相应地必须有访问获取和设定参数的方法加以包装。Graph类至少需要拥有一个包含所有点的数据结构(列表或者map等),相应地应该有新增顶点、访问顶点、新增连接边等方法。当然,为了实现Dijkstra算法(一种基本的最短路径算法),除了可以在Graph类里增加一个执行Dijkstra算法的方法以外,还需要在Vertex类里增加用于Dijkstra算法的一些参数:某一个顶点距离Dijkstra搜索起点的距离,以及一旦完成Dijkstra搜索需要回溯路径时,前驱顶点的信息。
在这里记录用python实现以上基本方法和Dijkstra算法的代码,因为Python中的现成的数据结构类型便于使用,比如字典dict类,很方便地能够构造一个类似于map的映射,而且Python的sort方法也特别好用。首先是自定义类顶点(Vertex)的代码,如上所述,为了满足BFS算法的执行和回溯的需要,Vertex类一共有四个参数:id(标记)、connections(邻接顶点字典,键值为邻接顶点,对应值为权重或者边长)、前驱顶点pre和从起点的距离distance。这二者在Dijkstra算法的执行过程中被赋值,在回溯时需要利用pre的信息。
Vertex类的方法中多数为基本的访问参数(get)-设定参数(set)的方法对,比如访问/设定邻接顶点信息的 add_neighbour(neighbour, weight) 和 get_connection() ,访问/设定前驱顶点信息的 get_pre() 和 set_pre(prev) ,以及访问/设定从起点起距离的 get_distance() 和 set_distance(dist) 。除此之外重载了字符串化方法,即__str__,这便于print函数和str函数将Vertex类转化为一个有意义的字符串。
class Vertex:
# 初始化构造函数,name为字符串,connections字典为<Vertex(class), weight(fl)>
# 前驱顶点pre,从起点距离distance,在Dijkstra执行赋值后有意义
def __init__(self, name):
self.id = name
self.pre = None
self.distance = float('inf')
self.connections = dict() # 重载字符串化函数,返回字符串
def __str__(self):
return str(self.id) + " connected to: " + str([x.id for x in self.connections]) # 增加相邻顶点,neighbour为Vertex类,weight为浮点型边权重
def add_neighbour(self, neighbour, weight=0):
self.connections[neighbour] = weight # 获取顶点id函数
def get_id(self):
return self.id # 获取顶点邻接点的函数,返回键值(Vertex)列表
def get_connections(self):
return self.connections.keys() # 获取顶点与邻接点边权重,传入Vertex类对象neighbour,返回weight(fl)
def get_weight(self, neighbour):
return self.connections[neighbour] # 获取顶点的距离(在BFS执行后使用)
def get_distance(self):
return self.distance # 获取前驱顶点(在Dijkstra执行后使用)
def get_pre(self):
return self.pre # 设定顶点的距离(在Dijkstra执行过程中调用)
def set_distance(self, dist):
self.distance = dist # 设定前驱顶点(在Dijkstra执行过程中调用)
def set_pre(self, prev):
self.pre = prev
图(Graph)类:拥有两个参数:顶点字典(顶点id:顶点Vertex类)和顶点数量。顶点数量这个参数似乎没什么用,除非是增加一个判断图是否为空的 isEmpty() 方法和 getSize() 方法可能用得着。
方法中包含新增顶点的 add_vertex(name) ,按照id获取顶点的 get_vertex(name) 。dict的数据结构给按照id访问顶点在空间上和时间上都创造了极大的方便,也为此参数中为顶点字典而不用顶点列表。添加边的方法 add_edge(vertex1, vertex2, weight) ,其中需要调用Vertex类中设定邻接点的方法。除此之外,Graph类还有一些重载的方法,比如重载contains,这个函数在类似于 3 in list(range(5)) 这样的语句中被调用,比如经常用的,判断某个元素是否在列表内等。重载迭代器__iter__,类似于 for item in list(range(10)): 这样的语句中被调用,有助于图中所有Vertex的遍历。基于迭代器还可以重载字符串化方法__str__。
最后就是Dijkstra算法。关于这个算法的信息准备记录在另一篇随笔中,基本思路是创建一个优先队列(即距离起始点路程从小到大的队列),每次查看列表中路程最短的点A,观察在这个顶点A的邻接点中,是否有可能因为通过顶点A而使得该邻接点的路程缩减。每次这样一轮操作结束以后就从队列中删去这个点A,然后把队列重新排列一遍。在以前课上的学习中,优先队列使用的是二叉堆(Binary Heap)实现,在这里我直接调用了Python内置的sort函数,虽然计算复杂度不敢保证,但是从后面用的实际例子的效率来说也没有任何影响。
class Graph:
# 无参数构造函数,vertex_dict为<id(str),Vertex>映射字典
def __init__(self):
self.vertex_dict = dict()
self.vertex_num = 0 # 增加顶点函数,传入新增顶点id
def add_vertex(self, name):
if name not in self.vertex_dict.keys():
new_vertex = Vertex(name)
self.vertex_dict[name] = new_vertex
self.vertex_num = self.vertex_num + 1 # 增加边函数, 传入顶点1名称、顶点2名称、权重
def add_edge(self, vertex1, vertex2, weight):
if vertex1 not in self.vertex_dict:
self.add_vertex(vertex1)
if vertex2 not in self.vertex_dict:
self.add_vertex(vertex2)
self.vertex_dict[vertex1].add_neighbour(self.vertex_dict[vertex2], weight)
self.vertex_dict[vertex2].add_neighbour(self.vertex_dict[vertex1], weight) # 按照id检索顶点函数,传入id,返回Vertex类
def get_vertex(self, name):
if name in self.vertex_dict.keys():
return self.vertex_dict[name]
else:
return None # 重载contains方法,传入id,返回bool值
def __contains__(self, item):
return item in self.vertex_dict # 重载迭代器,返回对应迭代器
def __iter__(self):
return iter(self.vertex_dict.values()) # 重载字符串化方法,返回字符串
def __str__(self):
o_str = str()
for item in self:
o_str = o_str + str(item) + '\n'
return o_str # Dijkstra算法,传入起点
def dijkstra_search(self, start):
# 优先队列priority
priority = list(self.vertex_dict.values())
# 起点距离置零
start.set_distance(0)
# 优先队列重排
priority.sort(key=lambda x: x.get_distance(), reverse=True)
while priority:
# 重排标记changed,若存在顶点发生distance变化则标记为True
changed = False
# 弹出最高优先顶点current
current = priority.pop()
# 遍历current邻接顶点
for vertex_tmp in current.get_connections():
dist_tmp = current.get_distance() + current.get_weight(vertex_tmp)
# 若发现优势路径则更改邻接顶点的distance和前驱顶点pre
if dist_tmp < vertex_tmp.get_distance():
vertex_tmp.set_distance(dist_tmp)
vertex_tmp.set_pre(current)
changed = True
# 若有更改则重排优先队列
if changed:
priority.sort(key=lambda x: x.get_distance(), reverse=True)
最后,和Dijkstra算法配合还需要一个路径回溯的方法。本来把回溯作为图类的一个方法也可以,但是由于通过访问顶点的pre参数已经可以回溯到上一个Vertex类,中间不涉及到图的操作,因此可以安全地把它写成一个静态方法的形式,即不放在Graph类中:
# 回溯路径函数,传入参数终点destination,返回路径列表list(Vertex)
def reverse_trace(destination):
current = destination
trace = [destination]
while current.get_pre():
current = current.get_pre()
trace.append(current)
trace.reverse()
return trace
这个函数返回的是一个路径中顺序出现的顶点Vertex类列表。
作为图的一个练习和测试,我拿了北京市的地铁信息。首先我在某一个文件夹里新建了编号为line1到line10的10个.txt文件,录入了10条地铁线的信息。后来又新增了13号线和15号线的信息。这些txt里的信息是这样储存的:a)一行一个站名,表示起点站;b)一行一个站名+一个数字,表示一个站和前一站的距离;c)一行两个站名+一个数字,表示两个站之间的距离。然后可以写一个读入的函数,把这些信息读进来,每个站名就是一个id,形成一张图,选定一个起点S以后执行Dijkstra算法,然后选定一个终点D回溯,就得到了从S到D的最短路径。
如果只是想得到站与站之间的最短距离,这个方法当然是合适的;不过,如果考虑到换乘因素,就会发现这个算法没有考虑到换乘的时间代价,这会使得搜索出一些需要换乘很多次的结果,而这在现实生活中并不是最快的。一个改进成本很小的方案是,把每个站所属的线路号码加在站名后面作为id的一部分,比如用“王府井1”作为一个id。同时,利用存储形式c来规定换乘站的间距,比如规定“海淀黄庄4”和“海淀黄庄10”的距离。这样一来就可以考虑换乘代价了。在这个思路下,Graph类相应的读入函数:
class Graph:
# 读取顶点文件(适用于地铁路线例子的方法),传入文件路径path和线路编号subway
def read_in(self, path, subway):
with open(path, 'r') as file:
# 按行读取文件
line = file.readline()
station = None
while line:
info_list = line.split()
# 当行中仅含有1个元素时,仅创建
if len(info_list) == 1:
station = info_list[0]
station = station + str(subway)
self.add_vertex(station)
# 当行中含有2个元素时,第1个元素为车站名称,第2个元素为距上个车站距离
elif len(info_list) == 2:
pre_station = station
[station, distance] = info_list
distance = int(distance)
station = station + str(subway)
self.add_vertex(station)
self.add_edge(pre_station, station, distance)
# 否则当行中含有3个元素,前2个元素为车站,第3个元素为前二者间距
else:
[station1, station2, distance] = info_list
distance = int(distance)
self.add_edge(station1, station2, distance)
line = file.readline()
最后是初始化函数和个人偏爱的交互式菜单代码,仅供参考:
# 初始化函数(适用于地铁路线例子的方法)
# 读入在file_path所示文件夹下的北京地铁线数据,返回生成的图
def initialize():
graph = Graph()
file_path = 'D:\Personal Documents\Project\BeijingSubway\line'
for i in range(1, 11):
path = file_path + str(i) + '.txt'
graph.read_in(path, i)
path = file_path + '13.txt'
graph.read_in(path, 13)
path = file_path + '15.txt'
graph.read_in(path, 15)
return graph # 交互式菜单函数(适用于地铁路线例子的方法),传入图
def route_find_menu(graph):
print('>> 寻找乘地铁最短路线,输入0退出')
start = input('>> 输入起始站名+地铁线(如“王府井1”):')
while start != '':
graph.breadth_first_search(graph.get_vertex(start))
destination = input('>> 输入终点站名+地铁线(如“王府井1”):')
if destination == '':
break
trace_list = reverse_trace(graph.get_vertex(destination))
print([x.get_id() for x in trace_list])
start = input('>> 输入起始站名:')
最后只要分别调用 graph1 = initialize() 创建实例,并且用 route_find_menu(graph1) 进入菜单即可。
图:无向图(Graph)基本方法及Dijkstra算法的实现 [Python]的更多相关文章
- 分析函数调用关系图(call graph)的几种方法
绘制函数调用关系图对理解大型程序大有帮助.我想大家都有过一边读源码(并在头脑中维护一个调用栈),一边在纸上画函数调用关系,然后整理成图的经历.如果运气好一点,借助调试器的单步跟踪功能和call sta ...
- 最短路径——Dijkstra算法和Floyd算法
Dijkstra算法概述 Dijkstra算法是由荷兰计算机科学家狄克斯特拉(Dijkstra)于1959 年提出的,因此又叫狄克斯特拉算法.是从一个顶点到其余各顶点的最短路径算法,解决的是有向图(无 ...
- 最短路径Dijkstra算法和Floyd算法整理、
转载自:http://www.cnblogs.com/biyeymyhjob/archive/2012/07/31/2615833.html 最短路径—Dijkstra算法和Floyd算法 Dijks ...
- 【转】最短路径——Dijkstra算法和Floyd算法
[转]最短路径--Dijkstra算法和Floyd算法 标签(空格分隔): 算法 本文是转载,原文在:最短路径-Dijkstra算法和Floyd算法 注意:以下代码 只是描述思路,没有测试过!! Di ...
- HTTP协议漫谈 C#实现图(Graph) C#实现二叉查找树 浅谈进程同步和互斥的概念 C#实现平衡多路查找树(B树)
HTTP协议漫谈 简介 园子里已经有不少介绍HTTP的的好文章.对HTTP的一些细节介绍的比较好,所以本篇文章不会对HTTP的细节进行深究,而是从够高和更结构化的角度将HTTP协议的元素进行分类讲 ...
- CSharpGL(28)得到高精度可定制字形贴图的极简方法
CSharpGL(28)得到高精度可定制字形贴图的极简方法 回顾 以前我用SharpFont实现了解析TTF文件从而获取字形贴图的功能,并最终实现了用OpenGL渲染文字. 使用SharpFont,美 ...
- 看开源代码利器—用Graphviz + CodeViz生成C/C++函数调用图(call graph)
一.Graphviz + CodeViz简单介绍 CodeViz是<Understanding The Linux Virtual Memory Manager>的作者 Mel Gorma ...
- 最短路径之Dijkstra算法和Floyd-Warshall算法
最短路径算法 最短路径算法通常用在寻找图中任意两个结点之间的最短路径或者是求全局最短路径,像是包括Dijkstra.A*.Bellman-Ford.SPFA(Bellman-Ford的改进版本).Fl ...
- 【转载】Dijkstra算法和Floyd算法的正确性证明
说明: 本文仅提供关于两个算法的正确性的证明,不涉及对算法的过程描述和实现细节 本人算法菜鸟一枚,提供的证明仅是自己的思路,不保证正确,仅供参考,若有错误,欢迎拍砖指正 ----------- ...
随机推荐
- mybatis由浅入深day01_ 4.11总结(parameterType_resultType_#{}和${}_selectOne和selectList_mybatis和hibernate本质区别和应用场景)
4.11 总结 4.11.1 parameterType 在映射文件中通过parameterType指定输入参数的类型.mybatis通过ognl从输入对象中获取参数值拼接在sql中. 4.11.2 ...
- 06python 之基本数据类型
数字 int(整型) 在32位机器上,整数的位数为32位,取值范围为-2**31~2**31-1,即-2147483648~2147483646 在64位机器上,整数的位数为64位,取值范围为-2** ...
- Linux printf 命令
printf 命令用来格式化输出,用法如下: [keysystem@localhost ~]$ printf "%s\n" 1 2 3 4 1 2 3 4 [keysystem@l ...
- php学习六:字符串
前言:越来越觉得php的强大之处了,不紧是数组,在字符串方面也可以看出它的优势,第一:方法多,集合了js,c,c#等多门语言的方法:第二:有许多方法是其他语言不具备的,如他的模糊比较,就是其他语言所没 ...
- mysql concat
CONCAT_WS() 代表 CONCAT With Separator ,是CONCAT()的特殊形式. 第一个参数是其它参数的分隔符.分隔符的位置放在要连接的两个字符串之间. 分隔符可以是一个字符 ...
- CentOS7安装Openvswitch 2.3.1 LTS
CentOS7安装Openvswitch 2.3.0 LTS,centos7openvswitch 一.环境: 宿主机:windows 8.1 update 3 虚拟机:vmware 11 虚拟机操作 ...
- webpack----entry
入口文件下对象的键值,不多说,上图: 其实app就等同于name,于是乎 dist下的index.html中引入的js,就是: <script type="text/javascrip ...
- kubernetes创建yaml,pod服务一直处于 ContainerCreating状态的原因查找与解决
最近刚刚入手研究kubernetes,运行容器的时候,发现一直处于ContainerCreating状态,悲了个催,刚入手就遇到了点麻烦,下面来讲讲如何查找问题及解决的 运行容器命令: kubectl ...
- linux如何设置用户权限
linux与用户权限设置: 1.添加用户 首先用adduser命令添加一个普通用户,命令如下: #adduser tommy //添加一个名为tommy的用户 #passwd tommy //修改密码 ...
- SSL安装方法一:在Windows Server 2008安装SSL证书(IIS 7.0)
购买的是GlobalSign 公司的通配符域名型SSL 大致的意思就是“通配符公用名填写*.域名.com,这个下面的所有子域名是不受数量限制的,*可以换成任意字符” 1 生成数字证书签名请求文件(CS ...