「笔记」如何优雅地卡 Spfa

写在前面

某碳基生物问我的一个问题,给他画了张图,觉得比较有意思就放上来了。

本文是带有主观性质的一些理解,作者水平有限,若有不当之处请不吝赐教。

原理

众所周知 Spfa 可以看做是 Bellman-Ford 的队列优化。
Bellman-Ford 每轮松弛会使最短路的边数至少 +1,而最短路的边数最多为 n1,则其复杂度上界是稳定的 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(nk) 的。取 k=n2,总更新次数是 O(n2) 级别的。

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的专栏

posted @   Luckyblock  阅读(7205)  评论(0编辑  收藏  举报
编辑推荐:
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
点击右上角即可分享
微信分享提示