「筆記」如何優雅地卡 Spfa


寫在前面

某碳基生物問我的一個問題,給他畫了張圖,覺得比較有意思就放上來了。

本文是帶有主觀性質的一些理解,作者水平有限,若有不當之處請不吝賜教。

原理

眾所周知 Spfa 可以看做是 Bellman-Ford 的隊列優化。
Bellman-Ford 每輪松弛會使最短路的邊數至少 \(+ 1\),而最短路的邊數最多為 \(n-1\),則其復雜度上界是穩定的 \(O(nm)\) 的。
Spfa 使用了隊列,改變了松弛的順序。雖然在隨機圖上表現優異,但復雜度上界沒有變,而且很容易構造數據使其復雜度到達上界。

以下是一份朴素的 Spfa 的代碼:

int dis[kMaxn], vis[kMaxn];
void Spfa(int s_) {
  std::queue <int> q;
  memset(vis, 0, sizeof (vis));
  memset(dis, 63, sizeof (dis));
  dis[s_] = 0, vis[s_] = true;
  q.push(s_);
  while (! q.empty()) {
    int u_ = q.front(); q.pop();
    vis[u_] = false;
    for (int i = head[u_]; i; i = ne[i]) {
      int v_ = v[i], w_ = w[i];
      if (dis[u_] + w_ < dis[v_]) { //Here!
        dis[v_] = dis[u_] + w_;
        if (! vis[v_]) {
          q.push(v_);
          vis[v_] = true;
        }
      }
    }
  }
}

可以發現,在代碼中 if (dis[u_] + w_ < dis[v_]) 的松弛判斷是具有貪心性質的。
卡 Spfa 的原理是利用松弛判斷的貪心性,通過誘導使得圖的某些部分在出隊后又重復入隊,被多次更新,造成大量時間的浪費。

卡最短路

考慮如何實現原理中提到的多次更新的情況,即使得到達某節點的最短路在算法中不斷被更新,造成該節點連接部分重復入隊的情況。
換句話說,需要誘導 Spfa 不斷進入到達某個點的次短路。並在進入該點最短路時造成相連部分的重復更新。

如果允許負權邊出現,一種顯然的想法是構造一條負權鏈,鏈上每個節點都指向一個菊花圖的支配點。在如下所示的鏈套菊花中,菊花圖會被更新 \(k\) 次,每次更新的復雜度是 \(O(n-k)\) 的。取 \(k = \frac{n}{2}\),總更新次數是 \(O(n^2)\) 級別的。

Spfa killer

而在正權圖上,根據 Spfa 的 Bfs 特性,可以考慮構造多個如下的存在多個次短路的網格狀結構。
對於某一個節點存在多條從起點到它的路徑。由於豎邊的權值為 0,包含橫邊和斜邊數相同的路徑長度相近。但由於包含邊數不同,這些路徑被遍歷的順序也不同。這就可能造成該節點的重復入隊,從而導致后繼節點被重復更新的情況。

Spfa killer

這里有一份來自 如何卡SPFA_yfzcsc的博客-CSDN博客 的 datamaker:

#include <bits/stdc++.h>
using namespace std;
struct edge {
  int u, v, w;
};
vector<edge> v;
int id[5000][5000], n = 9, tp, m = 42866 / n, a[1000000];
int r() {
  return rand();
  // return rand()<<13|rand();
}
int main() {
  freopen("in.txt", "w", stdout);
  srand(time(0));
  for (int i = 1; i <= n; ++i)
    for (int j = 1; j <= m; ++j) id[i][j] = ++tp, a[tp] = tp;
  //	random_shuffle(a+1,a+tp+1);
  int SIZE = 29989;
  for (int i = 1; i <= n; ++i)
    for (int j = 1; j <= m; ++j) {
      if (i < n) {
        v.push_back(edge{id[i][j], id[i + 1][j], 1});
        v.push_back(edge{id[i + 1][j], id[i][j], 1});
        if (j < m) {
          if (1)
            v.push_back(edge{id[i][j], id[i + 1][j + 1], r() % SIZE + 10});
          else
            v.push_back(edge{id[i + 1][j + 1], id[i][j], r() % SIZE + 10});
        }
      }
      if (j < m) {
        v.push_back(edge{id[i][j], id[i][j + 1], r() % SIZE + 10});
        v.push_back(edge{id[i][j + 1], id[i][j], r() % SIZE + 10});
      }
    }
  fprintf(stderr, "[%d,%d,%d]", v.size(), n, m);
  random_shuffle(v.begin(), v.end());
  //	printf("%d %d %d\n",tp,v.size(),2);
  printf("%d %d\n", tp, v.size());
  for (int i = 0; i < v.size(); ++i)
    printf("%d %d %d\n", a[v[i].u], a[v[i].v], v[i].w);
  //	for(int i=1;i<=10;++i)printf("%d ",a[id[1][10*i]]);
  //	printf("%d %d",a[1],a[2]);
}

如果你是一個毒瘤出題人,可以將上述兩種方式結合起來。在隨機網格圖的基礎上外掛誘導節點進入菊花圖,可以干掉大部分 Spfa。

卡 Spfa-dfs 判負環

Spfa-dfs 實際上是個假算法。在沒有負環的情況下它可以被卡到指數級。這有張圖:

Spfa killer

寫在最后

關於 Spfa 的各種優化的卡法詳見 fstqwq 的知乎回答

鳴謝

OI-Wiki

如何看待 SPFA 算法已死這種說法? - fstqwq 的回答 - 知乎 https://www.zhihu.com/question/292283275/answer/484871888

如何卡SPFA_yfzcsc的博客-CSDN博客

[HDOJ 4889] Scary Path Finding Algorithm [SPFA]_jinzhao1994的專欄


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM