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

引用內(nèi)容
松弛操作的原理是著名的定理:“三角形兩邊之和大于第三邊”,在信息學(xué)中我們叫它三角不等式。所謂對(duì)i,j進(jìn)行松弛,就是判定是否d[j]>d[i]+w[i,j],如果該式成立則將d[j]減小到d[i]+w[i,j],否則不動(dòng)。
(3)上一步中,我們認(rèn)為我們“改進(jìn)了”結(jié)點(diǎn)v的最短路徑,結(jié)點(diǎn)v的當(dāng)前路徑長(zhǎng)度D[v]相比于以前減小了一些,于是,與v相連的一些結(jié)點(diǎn)的路徑長(zhǎng)度可能會(huì)相應(yīng)地減小。注意,是可能,而不是一定。但即使如此,我們?nèi)匀灰獙加入到隊(duì)列中等待處理,以保證這些結(jié)點(diǎn)的路徑值在算法結(jié)束時(shí)被降至最優(yōu)。當(dāng)然,如果連接至v的邊較多,算法運(yùn)行中,結(jié)點(diǎn)v的路徑長(zhǎng)度可能會(huì)多次被改進(jìn),如果我們因此而將v加入隊(duì)列多次,后續(xù)的工作無(wú)疑是冗余的。這樣,就需要我們維護(hù)一個(gè)bool數(shù)組Inqueue[],來(lái)記錄每一個(gè)結(jié)點(diǎn)是否已經(jīng)在隊(duì)列中。我們僅將尚未加入隊(duì)列的點(diǎn)加入隊(duì)列。
算法能否結(jié)束?
對(duì)于不存在負(fù)權(quán)回路的圖來(lái)說(shuō),上述算法是一定會(huì)結(jié)束的。因?yàn)樗惴ㄔ诜磸?fù)優(yōu)化各個(gè)最短路徑長(zhǎng)度,總有一個(gè)時(shí)刻會(huì)進(jìn)入“無(wú)法再優(yōu)化”的局面,此時(shí)一旦隊(duì)列讀空,算法就結(jié)束了。然而,如果圖中存在一條權(quán)值為負(fù)的回路,就糟糕了,算法會(huì)在其上反復(fù)運(yùn)行,通過(guò)“繞圈”來(lái)無(wú)休止地試圖減小某些相關(guān)點(diǎn)的最短路徑值。假如我們不能保證圖中沒(méi)有負(fù)權(quán)回路,一種“結(jié)束條件”是必要的。這種結(jié)束條件是什么呢?
思考Bellman-Ford算法,它是如何結(jié)束的?顯然,最樸素的Bellman-Ford算法不管循環(huán)過(guò)程中發(fā)生了什么,一概要循環(huán)|V|-1遍才肯結(jié)束。憑直覺(jué)我們可以感到,SPFA算法“更聰明一些”,就是說(shuō)我們可以猜測(cè),假如在SPFA中,一個(gè)點(diǎn)進(jìn)入隊(duì)列——或者說(shuō)一個(gè)點(diǎn)被處理——超過(guò)了|V|次,那么就可以斷定圖中存在負(fù)權(quán)回路了。
最短路徑本身怎么輸出?
在一幅圖中,我們僅僅知道結(jié)點(diǎn)A到結(jié)點(diǎn)E的最短路徑長(zhǎng)度是73,有時(shí)候意義不大。這附圖如果是地圖的模型的話,在算出最短路徑長(zhǎng)度后,我們總要說(shuō)明“怎么走”才算真正解決了問(wèn)題。如何在計(jì)算過(guò)程中記錄下來(lái)最短路徑是怎么走的,并在最后將它輸出呢?
Path[]數(shù)組,Path[i]表示從S到i的最短路徑中,結(jié)點(diǎn)i之前的結(jié)點(diǎn)的編號(hào)。注意,是“之前”,不是“之后”。最短路徑算法的核心思想成為“松弛”,原理是三角形不等式,方法是上文已經(jīng)提及的。我們只需要在借助結(jié)點(diǎn)u對(duì)結(jié)點(diǎn)v進(jìn)行松弛的同時(shí),標(biāo)記下Path[v] = u,記錄的工作就完成了。
輸出時(shí)可能會(huì)遇到一點(diǎn)難處,我們記的是每個(gè)點(diǎn)“前面的”點(diǎn)是什么,輸出卻要從最前面往最后面輸,這不好辦。其實(shí)很好辦,見(jiàn)如下遞歸方法:

程序代碼
void PrintPath(int k){
if( Path[k] ) PrintPath(Path[k]);
fout<<k<<' ';
}
SPFA的代碼怎么寫(xiě)?
我寫(xiě)了鄰接表和鄰接矩陣兩種,兩者想像起來(lái)是那么的不同,算法的思路上實(shí)在區(qū)別不大,只是用不同方式詮釋“掃描”的過(guò)程而已。只給出SPFA的單個(gè)函數(shù),我不覺(jué)得很容易看懂,但是我仍然把兩個(gè)程序的SPFA函數(shù)放在下面。在日志的結(jié)尾處,有一個(gè)完整版文件下載。貼程序,首先是鄰接表的:

程序代碼
void SPFA(){
for(int i=1; i<=gv; i++)
Dist[i] = 100000;
Dist[S] = 0;
int closed = 0, open = 1;
queue[1] = S;
Inqueue[S] = true;
do{
closed++;
node *tmp = connect[queue[closed]];
Inqueue[queue[closed]] = false;
while(tmp != NULL){
if( Dist[tmp->key] > Dist[queue[closed]] + tmp->w ){
Dist[tmp->key] = Dist[queue[closed]] + tmp->w;
Path[tmp->key] = queue[closed];