单源最短路径快速算法(spfa)的python3.x实现
0. 写在最前面
最近比较忙呢,写的比较少了。抽空写了一下这篇文档,简陋勿喷~(后面准备做个算法包,包括基础的数据结构和算法,感觉任重而道远)
1. SPFA的简介[1]
SPFA(Shortest Path Faster Algorithm)算法是求单源最短路径的一种算法,它是Bellman-ford的队列优化,它是一种十分高效的最短路算法。
很多时候,给定的图存在负权边,这时类似Dijkstra等算法便没有了用武之地,而Bellman-Ford算法的复杂度又过高,SPFA算法便派上用场了。SPFA的复杂度大约是O(kE),k是每个点的平均进队次数(一般的,k是一个常数,在稀疏图中小于2)。
但是,SPFA算法稳定性较差,在稠密图中SPFA算法时间复杂度会退化。
实现方法:建立一个队列,初始时队列里只有起始点,在建立一个表格记录起始点到所有点的最短路径(该表格的初始值要赋为极大值,该点到他本身的路径赋为0)。然后执行松弛操作,用队列里有的点去刷新起始点到所有点的最短路,如果刷新成功且被刷新点不在队列中则把该点加入到队列最后。重复执行直到队列为空。
此外,SPFA算法还可以判断图中是否有负权环,即一个点入队次数超过N。
2. python实现[2]
"""
_spfa_simple算法参考【xunalove】在csdn中的帖子
_spfa_cut算法参考【火星十一郎】在cnblogs中的帖子
"""
from queue import Queue
import numpy as np
npa = np.array
npm = np.mat
def spfa(s, node=0, inf=np.inf, method='simple'):
"""
单源最短路的SPFA算法,Shortest Path Faster Algorithm
:param s:距离矩阵(邻接矩阵表示)其中s[i][j]代表i到j的距离
:param node:源点
:param inf:无穷大值
:param method:
'simple':简单算法,针对非负距离(注意不是非负环)有效
:return:
"""
if method == 'simple':
return _spfa_simple(s, node, inf)
elif method == 'cut':
return _spfa_cut(s, node, inf)
else:
raise ValueError("method not found")
def _spfa_simple(s, node=0, inf=np.inf):
"""
单源最短路径算法,
只对非负权值有效
:param s: 距离矩阵(邻接矩阵表示)其中s[i][j]代表i到j的距离
:param node:源点
:return:
"""
a = npa(s)
m, n = a.shape
if m != n:
raise ValueError("s 需要是方阵")
dis = np.ones(n) * inf
vis = np.zeros(n, dtype=np.int8)
dis[node] = 0
vis[node] = 1
que = Queue()
prenode = -np.ones(n, dtype=np.int8) # 记录前驱节点,没有则用-1表示
que.put(node)
while not que.empty():
v = que.get()
vis[v] = 0
for i in range(n):
temp = dis[v] + a[v][i]
if a[v][i] > 0 and dis[i] > temp:
dis[i] = temp # 修改最短路
prenode[i] = v
if vis[i] == 0: # 如果扩展节点i不在队列中,入队
que.put(i)
vis[i] = 1
return dis, prenode
def _spfa_cut(s, node=0, inf=np.inf):
"""
单源最短路径算法,
只对非负环有效
:param s: 距离矩阵(邻接矩阵表示)其中s[i][j]代表i到j的距离
:param node:源点
:return:
"""
a = npa(s)
m, n = a.shape
if m != n:
raise ValueError("s 需要是方阵")
count = np.zeros(n, dtype=np.int8)
dis = np.ones(n) * inf
vis = np.zeros(n, dtype=np.int8)
dis[node] = 0
vis[node] = 1
que = Queue()
prenode = -np.ones(n, dtype=np.int8) # 记录前驱节点,没有则用-1表示
que.put(node)
while not que.empty():
v = que.get()
vis[v] = 0
for i in range(n):
temp = dis[v] + a[v][i]
if dis[i] > temp:
dis[i] = temp # 修改最短路
prenode[i] = v
if vis[i] == 0: # 如果扩展节点i不在队列中,入队
count[i] += 1
if count[i] > n:
raise ValueError("输入有负环异常")
que.put(i)
vis[i] = 1
return dis, prenode
3. 改进和说明
- 现在的代码比较简陋,以后需要更多的补充。比如
- 没有优先队列优化,可以参考参考资料[4]优化
- 缺乏测试用例进行测试
- 缺少与迪杰斯特拉(Dijkstra)算法和贝尔曼-弗洛伊德(Bellman-Ford)算法的比较
- 算法更试用于稀疏图,而且时间效率并不稳定[4]
4. 写在最后
把这块代码上传到pypi.org的时候发现居然404错,说我没有验证邮箱。可能由于pypi在海外的关系,为了等验证邮箱居然花了半个小时。不吐不快。
参考资料
[1] cnblogs SPFA算法 2018-3-5
[2] 码云开源项目 python 3实现的spfa https://gitee.com/snowlandltd/snowland-algorithm-python/blob/master/SApy/graphtheory/spfa/_spfa.py 2018-3-5
[3] csdn 最快最好用的——spfa算法 2018-3-5 【比较推荐这篇,用C++和Pascal实现的】
[4] cnblogs SPFA算法学习笔记 2018-3-5 【同样推荐这篇,java实现,而且用优先队列优化】