計蒜客 - 闖關遊戲 | SPFA

今天分享一道有關 SPFA 單源最短路的算法題。

蒜頭君在玩一個很好玩的遊戲,這個遊戲一共有至多 100100 個地圖,其中地圖 11 是起點,房間 nn 是終點。有的地圖是補給站,可以加 kik_i 點體力,而有的地圖裏存在怪物,需要消耗 kik_i 點體力,地圖與地圖之間存在一些單向通道鏈接。 蒜頭君從 11 號地圖出發,有 100100 點初始體力。每進入一個地圖的時候,需要扣除或者增加相應的體力值。這個過程持續到走到終點,或者體力值歸零就會 Game Over。不過,他可以經過同個地圖任意次,且每次都需要接受該地圖的體力值。

輸入格式

11 行一個整數 nn (n100n\leq 100)。

22 ~ n+1n+1 行,每行第一個整數表示該地圖體力值變化。接下來是從該房間能到達的房間名單,第一個整數表示房間數,後面是能到達的房間編號。

5
0 1 2
-60 1 3
-60 1 4
20 1 5
0 0

輸出格式

若玩家能到達終點,輸出 Yes,否則輸出 No

No

首先我們來看一下什麼是 SPFA。

衆所周知,Dijkstra 算法不能處理有負權的圖,而 Bellman-ford 算法通過對圖進行 V1|V| - 1 次鬆弛操作,得到所有可能的最短路徑,而 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 的算法描述:

  1. d[i] 表示從源點 ss 到頂點 ii 的最短路,隊列 q 保存即將進行拓展的頂點列表,inq[i] 標識頂點 ii 是不是在隊列中;

  2. 初始隊列中僅包含源點 ss,且源點 ssd[s] = 0

  3. 取出隊列頭頂點 uu,掃描從頂點 uu 出發的每條邊,設每條邊的另一端爲 vv,邊 <u,v> 權值爲 ww,若 d[u] + w < d[v],則 將 d[v] 修改爲 d[u] + w,若 vv 不在隊列中,則將 vv 入隊。重複步驟直到隊列爲空。

  4. 最終 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] 大於頂點總數 nn,則表示該圖中包含負環。

很顯然,SPFA 的空間複雜度爲 O(V)\mathcal{O}(V)。如果頂點的平均入隊次數爲 kk,則 SPFA 的時間複雜度爲 O(kE)\mathcal{O}(kE),對於較爲隨機的稀疏圖,根據經驗 kk 一般不超過 44

對於稀疏圖而言,SPFA 相比堆優化的 Dijkstra 有很大的效率提升,但是對於稠密圖而言,SPFA 最壞爲 O(VE)\mathcal{O}(VE),遠差於堆優化 Dijkstra 的 O((V+E)logV)\mathcal{O}((V+E)logV)

在看完了 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]++,若等於 nn,說明有環,那麼蒜頭君就可以無限制地往這個點走,最終體力爲無窮大,再也不需要考慮其他點的體力值了,因此一定可以走到終點。所以直接 return,並直接輸出 Yes

最後的輸出結果就是 d[n] > 0

完整代碼參見:https://www.jxtxzzw.com/archives/5265

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章