最短路徑問題
在圖結構中,求解最短路徑問題有多種算法,Bellman-Ford是其中之一,它可以處理含有負權邊的情況,同樣是單源最短路徑算法,而之前講到的Dijkstra算法不能處理含有負權邊的情況。對應的代價就是其算法時間複雜度要高一些。後面我們會分析。
這裏能處理負權邊是針對有向圖的,因爲對無向圖來說,含有負權邊就意味着含有負權迴路,在有負權迴路的這種情況下求最短路徑是無解的,因爲每經過一次負權迴路,距離都會減少,就會無限循環下去。
在繼續往下講之前,先補充一個圖最短路徑的一個性質:最短路徑的子路徑也是最短路徑,數學描述如下:有向圖,設爲從點到點的一條最短路徑,且,設爲路徑中從點到點的子路徑,那麼也是這兩點之間的一條最短路徑。可用反證法來證明:
證明:如果將路徑分解爲,則有。假設存在一條從到的一條更短的路徑,。則新路徑權值爲,這與是最短路徑相矛盾。
Bellman-Ford算法
與Dijkstra算法類似,Bellman-Ford算法也是通過不斷的“鬆弛”操作來求得最終解。“鬆弛”就是如下的操作過程:表示與之間的權值,表示從源點到頂點的距離,若存在邊,使得:(即發現了優於當前的路徑),則更新,並更新路徑。可以看到每一次“鬆弛”都會更逼近最優解。Dijkstra算法通過優先隊列每次選擇當前未被處理過的距離最小的頂點,對該頂點未被處理過的邊進行鬆弛。而Bellman-Ford算法則簡單的鬆弛所有的邊,反覆執行次(爲頂點的的個數),時間複雜度。可以看出,Bellman-Ford鬆弛的次數遠多於Dijkstra,所以其時間複雜度相比Dijkstra要高。
僞代碼如下:
function BellmanFord(list vertices, list edges, vertex source)
// step 1 初始化, dist[v]表示源節點到頂點v的距離值,prev[v]表示頂點v的前驅頂點
for each vertex v in vertices
dist[v] = inf
prev[v] = null
dist[source] = 0
// step 2 迭代鬆弛|V|-1次
for i from 1 to size(vertices) -1
for each edge(u,v) with weight(u,v) in edges
if dist[u] + weight(u,v) < dist[v]
dist[v] = dist[u] + weight(u,v)
prev[v] = u
// step 3 檢查是否有負權迴路
for each edge(u,v) with weight(u,v) in edges
if dist[u] + weight(u,v) < dist[v]
error "檢測到負權迴路"
return dist[], prev[]
對算法的優化: 在實際應用中,Bellman-Ford算法其實不用迭代鬆弛次,理論上圖中存在的最大的路徑長度爲,實際上往往要小於這個,即,在次迭代鬆弛之前就已經收斂了,計算出最短路徑了,所以可在循環中設置判定,在某次循環中不再進行鬆弛時,表明當前已收斂,可退出步驟2,進行下一步檢查是否有負權迴路。
怎麼理解這個算法呢? 假設某頂點與源頂點沒有連通,即沒有邊,那麼這個點就不會被鬆弛,距離不會被更新,依舊爲無窮大。如果頂點與源頂點是連通的,在不存在負權迴路的情況下,一定存在一條最短路徑,這條最短路徑爲源點到之間的任意一條最短路徑(這裏,)。最大會有多少條邊呢?假設圖有個頂點,那麼有。在進行第一輪鬆弛時,被鬆弛的邊中一定會包含邊,結合文章開頭講到的最短路徑的子路徑也一定是最短路徑的性質,已經得到了其最短路徑,在第二輪鬆弛過程中,被鬆弛的邊中一定會包含,經過此次鬆弛後,也已經得到了其最短路徑。以此類推,在第輪鬆弛中,被鬆弛的邊中一定包含了邊,之後也得到其最短路徑。也就是說,凡是與源頂點最短路徑經過的邊數爲的頂點,在第輪鬆弛時一定會被確認(最短路徑被找到)。所以,我們需要鬆弛多少輪呢,最多次就可以了。
算法的數學證明可以參考《圖論》或《算法導論》中的證明過程。
代碼實現見bellman_ford.cpp。最後再分析一下時間複雜度,最壞的情況,這個比較好理解,最好的情況,一次鬆弛所有邊的操作就可以了,對應的就是邊鬆弛的順序恰好是最短路徑樹的生成順序。
算法的應用
其中一個應用就是路由協議了(距離向量協議),對此實現了一個路由協議測試工程,代碼見router。實現了一個通過路由表的方式進行的路由算法。