求最短路徑的算法有許多種,除了排序外,恐怕是OI界中解決同一類問題算法最多的了。最熟悉的無疑是Dijkstra,接著是Bellman-Ford,它們都可以求出由一個源點向其他各點的最短路徑;如果我們想要求出每一對頂點之間的最短路徑的話,還可以用Floyd-Warshall。
SPFA是這篇日志要寫的一種算法,它的性能非常好,代碼實現也并不復雜。特別是當圖的規模大,用鄰接矩陣存不下的時候,用SPFA則可以很方便地面對臨接表。每個人都寫過廣搜,SPFA的實現和廣搜非常相似。
如何求得最短路徑的長度值?
首先說明,SPFA是一種單源最短路徑算法,所以以下所說的“某點的最短路徑長度”,指的是“某點到源點的最短路徑長度”。
我們記源點為S,由源點到達點i的“當前最短路徑”為D[i],開始時將所有D[i]初始化為無窮大,D[S]則初始化為0。算法所要做的,就是在運行過程中,不斷嘗試減小D[]數組的元素,最終將其中每一個元素減小到實際的最短路徑。
過程中,我們要維護一個隊列,開始時將源點置于隊首,然后反復進行這樣的操作,直到隊列為空:
(1)從隊首取出一個結點u,掃描所有由u結點可以一步到達的結點,具體的掃描過程,隨存儲方式的不同而不同;
(2)一旦發現有這樣一個結點,記為v,滿足D[v] > D[u] + w(u, v),則將D[v]的值減小,減小到和D[u] + w(u, v)相等。其中,w(u, v)為圖中的邊u-v的長度,由于u-v必相鄰,所以這個長度一定已知(不然我們得到的也不叫一個完整的圖);這種操作叫做松弛。

(3)上一步中,我們認為我們“改進了”結點v的最短路徑,結點v的當前路徑長度D[v]相比于以前減小了一些,于是,與v相連的一些結點的路徑長度可能會相應地減小。注意,是可能,而不是一定。但即使如此,我們仍然要將v加入到隊列中等待處理,以保證這些結點的路徑值在算法結束時被降至最優。當然,如果連接至v的邊較多,算法運行中,結點v的路徑長度可能會多次被改進,如果我們因此而將v加入隊列多次,后續的工作無疑是冗余的。這樣,就需要我們維護一個bool數組Inqueue[],來記錄每一個結點是否已經在隊列中。我們僅將尚未加入隊列的點加入隊列。
算法能否結束?
對于不存在負權回路的圖來說,上述算法是一定會結束的。因為算法在反復優化各個最短路徑長度,總有一個時刻會進入“無法再優化”的局面,此時一旦隊列讀空,算法就結束了。然而,如果圖中存在一條權值為負的回路,就糟糕了,算法會在其上反復運行,通過“繞圈”來無休止地試圖減小某些相關點的最短路徑值。假如我們不能保證圖中沒有負權回路,一種“結束條件”是必要的。這種結束條件是什么呢?
思考Bellman-Ford算法,它是如何結束的?顯然,最樸素的Bellman-Ford算法不管循環過程中發生了什么,一概要循環|V|-1遍才肯結束。憑直覺我們可以感到,SPFA算法“更聰明一些”,就是說我們可以猜測,假如在SPFA中,一個點進入隊列——或者說一個點被處理——超過了|V|次,那么就可以斷定圖中存在負權回路了。
最短路徑本身怎么輸出?
在一幅圖中,我們僅僅知道結點A到結點E的最短路徑長度是73,有時候意義不大。這附圖如果是地圖的模型的話,在算出最短路徑長度后,我們總要說明“怎么走”才算真正解決了問題。如何在計算過程中記錄下來最短路徑是怎么走的,并在最后將它輸出呢?
Path[]數組,Path[i]表示從S到i的最短路徑中,結點i之前的結點的編號。注意,是“之前”,不是“之后”。最短路徑算法的核心思想成為“松弛”,原理是三角形不等式,方法是上文已經提及的。我們只需要在借助結點u對結點v進行松弛的同時,標記下Path[v] = u,記錄的工作就完成了。
輸出時可能會遇到一點難處,我們記的是每個點“前面的”點是什么,輸出卻要從最前面往最后面輸,這不好辦。其實很好辦,見如下遞歸方法:

}
SPFA的代碼怎么寫?
我寫了鄰接表和鄰接矩陣兩種,兩者想像起來是那么的不同,算法的思路上實在區別不大,只是用不同方式詮釋“掃描”的過程而已。只給出SPFA的單個函數,我不覺得很容易看懂,但是我仍然把兩個程序的SPFA函數放在下面。在日志的結尾處,有一個完整版文件下載。貼程序,首先是鄰接表的:
