SPFA 優化
眾所周知,SPFA 它死了
但有些時候你會嫌支持負邊的 dijkstra 麻煩,於是不得不選擇 SPFA
那么,你需要 SPFA 優化!
基礎篇
如果你是只想看代碼的小萌新,請看這里。
否則可以直接跳過這一篇。
SLF 優化
我們可以參考一下 dijkstra 的思路。
dijkstra 每次取隊列中的最小值,減少了同一節點入隊次數和更新 dis 次數,於是避免了死亡
那么我們感性理解一下,如果我們在 SPFA 中每次也取較小 dis 的點是不是也可以得到一定的優化呢?
我們可以通過雙端隊列,在節點入隊時實現這一點:把需要入隊的點的 dis 與隊首的比較一下,如果小於隊首則將節點從隊首入隊,否則從隊尾入隊。
LLL 優化
其實本質原理與 SLF 一樣,只是實現方式不同啦 ~
同樣使用雙端隊列,我們額外維護一下隊列中所有點的 dis 的平均值,把即將入隊的點與這個平均值比較,決定從隊首還是隊尾入隊。
貼(偽)代碼
顯而易見,這兩個優化並不相矛盾,所以我們可以同時使用!
int dis[N];
//起點是 S
inline void SPFA(int S)
{
//下面這三個東西只在該函數中使用,於是把它們放入函數里,避免被外界調用,不易重名、出錯
static deque<int> q;//雙端隊列
static bool inque[N];//記錄每個點是否在隊列中
static int sum = 0;//因為平均數不方便維護,我們選擇維護隊列中 dis 的和
//TODO: 此處應初始化 dis 數組為 inf
dis[S] = 0;
q.push_back(S);
while (q.size()) {
int x = q.front();
q.pop_front();
inque[x] = 0, sum -= dis[x];//出隊維護qaq
for (/*TODO: 遍歷 x 的所有出邊: 出邊 e 和 對應點 y */) {
if (dis[y] > dis[x] + w[e]) {
dis[y] = dis[x] + w[e];
if (!inque[y]) {
//此處為兩種優化的核心
//一定要注意隊列為空的特殊情況
if (q.empty() || dis[y] < dis[q.front()] || dis[y]*q.size() < sum)
q.push_front(y);
else q.push_back(y);
//
inque[y] = 1, sum += dis[y];//入隊記得維護qwq
}
}
}
}
}
十分簡單好寫呢,為什么不隨手寫一個呢
提高篇
這一篇沒有代碼。
看這里的話,前面的東西是非必須的。
SLF 優化
每次入隊時,與隊首比較,若大於隊首則插入隊尾,反之插入隊首。
HACK:鏈套菊花圖
LLL 優化
每次入隊時,與隊內平均值比較,若大於隊首則插入隊尾,反之插入隊首。
HACK:起點連一條巨大邊權的邊,使得隊內均值一直很大。
SLF 帶容錯
每次入隊時,與隊首比較,若比隊首大超過一定閾值則插入隊尾,反之插入隊首。
HACK:開大邊權,總和超過 \(10^{12}\) 時優化失效顯著。
SLF + swap
出隊時,若隊首大於隊尾,則交換首尾。
HACK:鏈套菊花圖 + 外掛誘導節點
mcfs 優化
在第 \([L,R]\) 次訪問這個節點時,將其隊首入隊,否則隊尾入隊。
HACK:網格圖表現優異,菊花圖表現差勁。
P.S. 此優化加上SLF 帶容錯 效果顯著提升
總結
所有的 SPFA 優化都是在試圖使隊列更接近優先隊列,所以 SPFA 永遠只能是一些情況下接近 dijsktra ,而在特定圖中被卡回 \(O(MN)\) 的原形。