最短路徑經典演算法其二Bellman-Ford
技術標籤:演算法小課堂python資料結構演算法bellman–ford algorithmdijkstra
最短路徑經典演算法其二Bellman-Ford
前言
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}
第二步裡由於節點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。我們下期來繼續填坑。