前言
hello,各位快要回家happy的盆友们,现在是布谷布谷鸟的算法小课堂。
上周我们讲了 Dijkstra最短路径搜索算法,还用BFS的思想实现了它的第二个版本。还记得它的核心思想吗?(疯狂翻博客ing
就是不断找到当前可到达的新的最近点,它相当于中介的角色,我们用其与周围的邻接点的权重来更新起点到这些邻接点的距离。
今天我们来讲解另一种最短路径算法——Bellman-Ford算法。
Bellman-Ford 原理讲解
要理解它的思路,首先需要有个提前,那就是一个不含有环且,n个节点的有向图,从起点到达终点(起点不算)最短路最多只经过n-1个节点,这个很好理解,就是最短路上每个节点都利用到了。
为什么说这里要求不含环呢,环即指图里有节点形成的回环,有零环,正环和负环。其中,零环和正环去掉后并不影响最短路的长度,因为本身他们就是多余的,实际理解就是不去兜这个圈子。
而负环一旦存在,最短路就不会存在了。 想想是为啥?因为既然有负权重的路,而且还是环路,那么每经过一次环路,最短路的长度都会减少,即不存在最短路,而负权重在实际上也不会经常遇见。
言归正传,既然最短路最多有n-1个节点,那么是不是在一个循环里如果我能找到一个不同的最短点并更新距离,最多只需要n-1次循环就可以找到最短路。没错,Bellman_Ford就是利用这个简单的想法。
今天我们沿用上一次的例子并稍作修改:
根据上面的思路,我们每一轮循环确定一个最近点,假设我们的路径输入是:
u | v | weight |
---|---|---|
0 | 1 | 9 |
2 | 3 | 2 |
1 | 2 | 5 |
1 | 3 | 20 |
一开始起点本身到自己的距离为0,到所有其他节点的初始距离设置为INF
第一轮循环,我们的过程如下:
0 → 1 : D i s [ 0 ] = 0 , D i s [ 1 ] = inf > D i s [ 0 ] + w [ 0 ] [ 1 ] = 9 ; 2 → 3 : D i s [ 2 ] = inf 未 到 达 节 点 2 , 无 法 更 新 ; 1 → 2 : D i s [ 1 ] ≠ inf , D i s [ 2 ] = inf > D i s [ 1 ] + w [ 1 ] [ 2 ] ; 1 → 3 : D i s [ 1 ] ≠ inf , D i s [ 3 ] = inf > D i s [ 1 ] + w [ 1 ] [ 3 ] \begin{aligned} 0 &\rightarrow \ 1:Dis[0]=0 \ , \ Dis[1]=\inf \gt Dis[0]+w[0][1]=9; \\ 2 &\rightarrow \ 3:Dis[2]=\inf \ 未到达节点2,无法更新; \\ 1 &\rightarrow \ 2:Dis[1]\neq \inf \ ,\ Dis[2]=\inf \gt Dis[1]+w[1][2];\\ 1 &\rightarrow \ 3:Dis[1]\neq \inf \ ,\ Dis[3]=\inf \gt Dis[1]+w[1][3] \end{aligned} 0211→ 1:Dis[0]=0 , Dis[1]=inf>Dis[0]+w[0][1]=9;→ 3:Dis[2]=inf 未到达节点2,无法更新;→ 2:Dis[1]=inf , Dis[2]=inf>Dis[1]+w[1][2];→ 3:Dis[1]=inf , Dis[3]=inf>Dis[1]+w[1][3]
第二步里由于节点2还未到达,所以无法更新,但是我们通过节点1已经更新了到达节点2和节点3的距离。这里,可以发现一次循环里最差的情况是只更新到一个最短节点,但大多数情况数量是更多的,所以找到最短路径的速度其实是很快的。
接下来就是看看在第二次循环里能不能通过节点2到节点3的路径使起点到达节点3的距离更短:
2 → 3 : D i s [ 0 ] ≠ inf , D i s [ 3 ] = 20 > D i s [ 2 ] + w [ 2 ] [ 3 ] = 16 2 \rightarrow \ 3:Dis[0]\neq \inf \ ,\ Dis[3]=20 \gt Dis[2]+w[2][3]=16 2→ 3:Dis[0]=inf , Dis[3]=20>Dis[2]+w[2][3]=16
代码实现
按照上面的思路,我们可以实现我们的想法:
def bellman_ford(directed=True):
"""
bellman_ford Implementation
Args:
directed (bool, optional): [whether directed graph or not]. Defaults to True.
"""
# 适用于有向图
import time
limit = 10000
# num of node, num of links, start_node, end_node
n, k, s, e= list(map(int, input().split()))
# path weight
W = [list(map(int, input().split())) for i in range(k)]
start=time.time()
Dis = [limit for i in range(n+1)] # Dis[i]表示起点到节点i的距离,初始设为一个较大的值
Dis[s] = 0 # 起点到自身为0
# fresh flag
f = True
while f:
# 当没有更新时,退出循环
f = False
# 全盘扫描降距
for d in W:
u, v, dis = d
# 当已存在有起点到u的路径,尝试是否通过u->v可使起点到v的路径变短,本质是贪心加循环
if Dis[u] != limit:
Dis[v], f = (Dis[u]+dis, True) if Dis[u]+dis < Dis[v] else (Dis[v], f)
# show answer
print('Shortest distance from s to e: {}'.format(Dis[e]))
print('Time used: {:.5f}s'.format(time.time()-start))
return
样例测试
测试一下上面的例子:
test_example:
7 9 0 6
0 1 9
1 2 5
2 3 2
1 3 20
3 4 14
4 5 3
3 5 8
5 6 10
6 1 7
>>> bellman_ford()
Shortest distance from s to e: 34
Time used: 0.01303s
BFS实现
同样的,我们也可以用BFS的思想来实现Bellman_Ford算法,但是这里我们不需要得到每次循环里新一个离起点最近的节点,所以不需要优先队列,普通的FIFO队列即可。
然后节点也是可以重复进入的,因为是全盘搜索,不需要标记节点是否访问过,只要可以进入松弛条件,那就是有意义的更新。
我们还可以加入是否存在负环的判断,来避免不存在最短路的情况(参考《算法竞赛入门经典》刘汝佳著 [Page-364]), 参考上一次的BFS_Dijkstra,我们可以简单修改:
def BFS_bellman_ford(directed=True):
"""
Functions: Implementation of bellman_ford using BFS and PriorityQueue
Args:
directed (bool, optional): [whether directed graph or not]. Defaults to True.
"""
import time
from queue import Queue
# init distance
limit = 10000
# number of nodes, number of links, start_index, end_index
N, K, s, e = list(map(int, input().split()))
start = time.time()
# graph mat
ad_mat = [[0 for i in range(N)] for j in range(N)]
# distance to start_node
Dis = [limit for i in range(N)]
# number of adjacent nodes of one node
G = [[] for i in range(N)]
# use links to fresh graph mat
for i in range(K):
u, v, w = list(map(int, input().split()))
ad_mat[u][v] = w
G[u].append(v)
if directed == False:
ad_mat[v][u] = w
G[v].append(u)
# init distance of start_node
Dis[s] = 0
# counter the num of entering some nodes
cnt = [0 for i in range(N)]
# Queue object definition
class queue_obj:
def __init__(self, s):
self.s = s # node index
# BFS with Queue
Q = Queue()
Q.put(queue_obj(s))
while Q.qsize() != 0:
node = Q.get()
s = node.s
# fresh distance
for i in range(len(G[s])):
ad_node = G[s][i]
if Dis[ad_node] > (ad_mat[s][ad_node]+Dis[s]):
Dis[ad_node] = ad_mat[s][ad_node]+Dis[s]
Q.put(queue_obj(ad_node))
cnt[ad_node] += 1
# negative loop check
if cnt[ad_node] > N:
print("The graph has negative circle path")
loop= ''.join(str(k)+' ' for k in range(N) if cnt[k]==cnt[ad_node] or cnt[k]==cnt[ad_node]-1)
print("Checked nodes are {}".format(loop))
return
print('Shortest distance from s to e: {}'.format(Dis[e]))
print('Time used: {:.5f}s'.format(time.time()-start))
return
样例测试
先来测试刚才的例子:
test_example:
7 9 0 6
0 1 9
1 2 5
2 3 2
1 3 20
3 4 14
4 5 3
3 5 8
5 6 10
6 1 7
>>> BFS_bellman_ford()
Shortest distance from s to e: 34
Time used: 0.01500s
我们来测试一下检测负环的功能:
我们加入节点3到节点1的路径,并把节点1到节点2的路径权重改为负,这样就构成了由节点1,2,3所组成的负环。
这里我们判断负环的逻辑是,当存在负环,算法会不断的进入负环中的点,而我们又有n个节点无环有向图最短路径最多n-1个节点,所以查询进入节点的次数,如果超过n,则可以说明存在负环。
test_example:
7 10 0 6
0 1 9
1 2 -5
2 3 2
1 3 20
3 1 2
3 4 14
4 5 3
3 5 8
5 6 10
6 1 7
>>> BFS_bellman_ford()
The graph has negative circle path
Checked nodes are 1 2 3
可以看到找到了可疑的负环节点1,2,3,是符合图示的例子的。
总结
三大最短路径经典算法讲了两个,还差一个Flody,相信大家dddd。我们下期来继续填坑。