今天分享一道有關 SPFA 單源最短路的算法題。
蒜頭君在玩一個很好玩的遊戲,這個遊戲一共有至多 個地圖,其中地圖 是起點,房間 是終點。有的地圖是補給站,可以加 點體力,而有的地圖裏存在怪物,需要消耗 點體力,地圖與地圖之間存在一些單向通道鏈接。 蒜頭君從 號地圖出發,有 點初始體力。每進入一個地圖的時候,需要扣除或者增加相應的體力值。這個過程持續到走到終點,或者體力值歸零就會 Game Over。不過,他可以經過同個地圖任意次,且每次都需要接受該地圖的體力值。
輸入格式
第 行一個整數 ()。
第 ~ 行,每行第一個整數表示該地圖體力值變化。接下來是從該房間能到達的房間名單,第一個整數表示房間數,後面是能到達的房間編號。
5
0 1 2
-60 1 3
-60 1 4
20 1 5
0 0
輸出格式
若玩家能到達終點,輸出 Yes
,否則輸出 No
。
No
首先我們來看一下什麼是 SPFA。
衆所周知,Dijkstra 算法不能處理有負權的圖,而 Bellman-ford 算法通過對圖進行 次鬆弛操作,得到所有可能的最短路徑,而 SPFA(Shortest Path Faster Algorithm)通常被認爲是 Bellman-ford 算法的隊列優化,在代碼形式上接近於寬度優先搜索 BFS,是一個在實踐中非常高效的單源最短路算法。
需要指出的是,SPFA 的本質是 Bellman-ford 算法的隊列優化,由於 SPFA 沒有改變 Bellaman-ford 的時間複雜度,國外一般來說不認爲 SPFA 是一個新的算法,而僅僅是 Bellman-ford 的隊列優化。
在一定程度上,可以認爲 SPFA 是由 BFS 的思想轉化而來:從不含邊權或者說邊權爲 1 個單位長度的圖上的 BFS,推廣到帶權圖上,就得到了 SPFA。SPFA 與 BFS 的不同在於,BFS 中一個點出了隊列就不可能重新進入隊列,但是 SPFA 中一個點可能在出隊列之後再次被放入隊列,也就是一個點改進過其它的點之後,過了一段時間可能本身被改進,於是再次用來改進其它的點,這樣反覆迭代下去。
SPFA 可以處理任意不含負環(負環是指總邊權和爲負數的環)的圖的最短路,並能判斷圖中是否存在負環。
有了 BFS 的基礎,我們很容易得到 SPFA 的算法描述:
-
d[i]
表示從源點 到頂點 的最短路,隊列q
保存即將進行拓展的頂點列表,inq[i]
標識頂點 是不是在隊列中; -
初始隊列中僅包含源點 ,且源點 的
d[s] = 0
。 -
取出隊列頭頂點 ,掃描從頂點 出發的每條邊,設每條邊的另一端爲 ,邊
<u,v>
權值爲 ,若d[u] + w < d[v]
,則 將d[v]
修改爲d[u] + w
,若 不在隊列中,則將 入隊。重複步驟直到隊列爲空。 -
最終
d[]
數組就是從源點出發到每個頂點的最短路距離。如果一個頂點從沒有入隊,則說明沒有從源點到該頂點的路徑。
void spfa(int s) {
memset(inq, 0, sizeof(inq));
memset(d, 0x3f, sizeof(d));
d[s] = 0;
inq[s] = true;
queue<int> q;
q.push(s);
while (!q.empty()) {
int u = q.front();
q.pop();
inq[u] = false;
for (int i = p[u]; i != -1; i = e[i].next) {
int v = e[i].v;
if (d[u] + e[i].w < d[v]) {
d[v] = d[u] + e[i].w;
if (!inq[v]) {
q.push(v);
inq[v] = true;
}
}
}
}
}
在進行 SPFA 時,用一個數組 cnt[i]
來標記每個頂點入隊次數。如果一個頂點入隊次數 cnt[i]
大於頂點總數 ,則表示該圖中包含負環。
很顯然,SPFA 的空間複雜度爲 。如果頂點的平均入隊次數爲 ,則 SPFA 的時間複雜度爲 ,對於較爲隨機的稀疏圖,根據經驗 一般不超過 。
對於稀疏圖而言,SPFA 相比堆優化的 Dijkstra 有很大的效率提升,但是對於稠密圖而言,SPFA 最壞爲 ,遠差於堆優化 Dijkstra 的 。
在看完了 SPFA 之後,這道題就比較容易了。
在套用 SPFA 模板的時候,注意要初始化 d[]
數組爲負無窮大,並設置起點的體力值爲 d[s] = 100
。
然後在 SPFA 的判斷條件中 if (d[u] + e[i].w < d[v])
改成 if (d[u] + e[i].w > d[v])
因爲我們需要保留體力最大的。
另外在每一個點入隊以後,令 cnt[i]++
,若等於 ,說明有環,那麼蒜頭君就可以無限制地往這個點走,最終體力爲無窮大,再也不需要考慮其他點的體力值了,因此一定可以走到終點。所以直接 return,並直接輸出 Yes
。
最後的輸出結果就是 d[n] > 0
。