點擊上方藍字,關注並星標,和我一起學技術。node

今天是算法數據結構專題的第33篇文章,咱們一塊兒來聊聊最短路問題。web
最短路問題也屬於圖論算法之一,解決的是在一張有向圖當中點與點之間的最短距離問題。最短路算法有不少,比較經常使用的有bellman-ford、dijkstra、floyd、spfa等等。這些算法當中主要能夠分紅兩個分支,其中一個是bellman-ford及其衍生出來的spfa,另一個分支是dijkstra以及其優化版本。floyd複雜度比較高,通常不太經常使用。算法
咱們今天的文章介紹的是bellman-ford以及它的衍生版本spfa算法。數組
圖的存儲
咱們要對一張有向圖計算最短路,那麼咱們首先要作的就是將一張圖存儲下來。關於圖的存儲的數據結構,經常使用的方法有不少種。最簡單的是鄰接矩陣,所謂的鄰接矩陣就是用一個二維矩陣存儲每兩個點之間的距離。若是兩個點之間沒有邊相連,那麼設爲無窮大。微信
能夠參考一下下圖。數據結構

這種方法的好處是很是直觀,實現也很簡單,可是缺點也很明顯。這個二維矩陣所消耗的空間複雜度是 ,這裏的V指的是頂點的數量。當頂點的數量稍稍大一些以後,帶來的開銷是很是龐大的。通常狀況下咱們的圖的邊的密集程度是不高的,也就是說大量點和點之間沒有邊相連,咱們浪費了不少空間。app
針對鄰接矩陣浪費空間的問題,後來又提出了一種新的數據結構就是鄰接表。編輯器
所謂的鄰接表也就是說咱們把頂點一字排開存入數組當中,每一個頂點對應一條鏈表。這條鏈表當中存儲了這個點能夠到達的其餘點的信息。好比下圖當中V1點能夠連通V2和V3,V1在數組當中的下標爲0,因此下標爲0的元素是一個存儲了V2和V3下標的鏈表,也就是圖中的1和2。svg

鄰接表的好處是能夠最大化利用空間,有多少條邊存儲多少信息。可是也有缺點,除了實現稍稍複雜一點以外,另一個明顯的缺點就是咱們沒辦法直接判斷兩點之間是否有邊存在,必需要遍歷鏈表才能夠。flex
除了鄰接矩陣和鄰接表以外,還有一些其餘的數據結構能夠完成圖的存儲。好比前向星、邊集數組、鏈式前向星等等。這些數據結構並無比鄰接表有質的突破,對於非算法競賽同窗來講,可以熟練用好鄰接表也就足夠了。
Bellman-Ford算法
剛纔上面描述當中提到的算法除了floyd算法是計算的全部點對之間的最短距離以外,其餘算法解決的都是單源點最短路的問題。所謂的單源點能夠簡單理解成單個的出發點,也就是說咱們求的是從圖上的一個點出發去往其餘每一個點的最短距離。既然如此,咱們的出發點以及到達點都是肯定的,不肯定的只是它們之間的距離而已。
爲何咱們會將bellman-ford算法和dijkstra算法區分開呢?由於二者的底層邏輯是不一樣的,bellman-ford算法的底層邏輯是動態規劃, 而dijkstra算法的底層邏輯是貪心。
bellman-ford算法的得名也和人有關,咱們以前在介紹KMP算法的時候曾經說過。因爲英文表意能力不強,因此不少算法和公式都是以人名來取名。bellman-ford是Richard Bellman 和 Lester Ford 分別發表的,實際上還有一個叫Edward F. Moore也發表了這個算法,因此有的地方會稱爲是Bellman-Ford-Moore 算法。
算法的原理很是簡單,利用了動態規劃的思想來維護源點出發到各個點的最短距離。
它的核心思惟是鬆弛,所謂的鬆弛能夠理解成找到了更短的路徑對原路徑進行更新。對於一個有V個節點的有向圖進行V-1輪鬆弛,從而找到源點到全部點的最短距離。
初始的時候咱們會用一個數組記錄源點到其餘全部點的距離,對於與源點直接相連的點來講,這個距離就是它和源點的距離不然則是無窮大。對於第一輪鬆弛來講,咱們尋找的是源點出發通過一個點到達其餘點的最短距離。咱們用s表明源點,咱們尋找的就是s通過點a到達點b,使得距離小於s直接到b的距離。
第二輪鬆弛就是尋找的s通過兩個點到達第三個點的最短距離,同理,對於一個有V個點的圖來講,兩個點之間最多通過V-1個點,因此咱們須要V-1輪鬆弛操做。
它的僞代碼很是簡單,咱們直接來看:
for (var i = 0; i < n - 1; i++) {
for (var j = 0; j < m; j++) {//對m條邊進行循環
var edge = edges[j];
// 鬆弛操做
if (distance[edge.to] > distance[edge.from] + edge.weight ){
distance[edge.to] = distance[edge.from] + edge.weight;
}
}
}
Bellman-ford的算法很好理解,實現也不難,可是它有一個缺點就是複雜度很高。咱們前面說了一共須要V-1輪鬆弛,每一輪鬆弛咱們都須要遍歷E條邊,因此總體的複雜度是 ,E指的是邊的數量。想一想看,假設對於一個有1w個頂點,10w條邊的圖來講,這個算法是顯然沒法得出結果的。
因此爲了提升算法的可用性,咱們必須對這個算法進行優化。咱們來分析一下複雜度巨大的緣由,主要在兩個地方,一個地方是咱們鬆弛了V-1次,另一個地方是咱們枚舉了全部的邊。鬆弛V-1次是不可避免的,由於可能存在極端的狀況須要V-1次鬆弛才能夠達成。但咱們每次都枚舉了全部的邊感受有點浪費,由於其中大部分的邊是不可能達成新的鬆弛的。那有沒有辦法咱們篩選出來可能構成新的鬆弛的邊呢?
針對這個問題的思考和優化引出了新的算法——spfa。
SPFA算法
SPFA算法的英文全稱是Shortest Path Faster Algorithm,從名字上咱們就看得出來這個算法的最大特色就是快。它比Bellman-ford要快上許多倍,它的複雜度是 ,這裏的k是一個小於等於2的常數。
SPFA的核心原理和Bellman-ford算法是同樣的,也是對點的鬆弛。只不過它優化了複雜度,優化的方法也很簡單,用一個隊列維護了可能存在新的鬆弛的點。這樣咱們每次從這些點出發去尋找其餘能夠鬆弛的點加入隊列,這裏面的原理很簡單,只有被鬆弛過的點纔有可能去鬆弛其餘的點。
SPFA的代碼也很短,實現起來難度很低,單單從代碼上來看和普通的寬搜區別並不大。
import sys
from queue import Queue
que = Queue()
# 鄰接表存儲邊
edges = [[]]
# 維護是否在隊列當中
visited = [False for _ in range(V)]
dis = [sys.maxsize for _ in range(V)]
dis[s] = 0
que.put(s)
while not que.emtpy():
u = que.get()
for v, l in edges[u]:
if dis[u] + l < dis[v]:
dis[v] = dis[u] + l
if not visited[v]:
que.add(v)
dis[u] = False
到這裏,關於Bellman-ford和SPFA算法的介紹就結束了,須要提醒一點,這兩個算法都不能處理負環的狀況。也就是權重之和是負數的環,這樣會無限鬆弛陷入死循環當中,能夠在求最短路以前經過拓撲排序排查,也能夠記錄每一個點進入隊列的次數,經過設置閾值的方式進行排除。
今天的文章到這裏就結束了,若是喜歡本文的話,請來一波素質三連,給我一點支持吧(關注、在看、點贊)。
- END -本文分享自微信公衆號 - TechFlow(techflow2019)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。