SPFA
updata 2025.8.24 修复了一些证明错误
易写,支持负权,可判负环,可以求最短路,也可以最长路,什么都行。就是容易被卡qwq,(退化为 Bellman_Ford)。所以SPFA他死了。是 Bellman_Ford 算法的队列优化版。
使用范围
支持负权,可以处理负环,可判负环,可以求最短路,也可以求最长路。
平均时间复杂度 \(\operatorname O(m)\),极限时间复杂度为 \(\operatorname O(nm)\) 即退化成 Bellman_Ford。
如上面所说,容易被卡成 \(\operatorname O(nm)\) 的时间复杂度。
中心思想
其算法正确性来自 Bellman_Ford 算法。
观察 Bellman_Ford 算法,可以发现,实际上只有上次被松弛的点,才能有可能引起下次操作中边的松弛,于是可以用一个队列,存入“可能引起下次松弛的点”,只利用这里面的点即可。
值得注意的是,这个队列中的点不需要重复,它并没有意义,所以对于上面所说的点,我们要判断是否在队列之内。
代码
给出两种代码,一种手写循环队列,一种用 STL 队列。
如果不用 STL 队列,一定要手写队列,因为一个点可能会入队多次,如果不循环,可能会出界。因此手写要用循环队列。是手写还是直接 STL 就看个人习惯了。
值得一说的特性,因为 \(st\) 数组内存的是每个点是否进队,而当 SPFA 结束时,队列里一定没有点,因此 \(st\) 数组全为空。
循环队列
int spfa(int S, int T)
{
memset(dist, 0x3f, sizeof dist);
memset(st, 0, sizeof st);
int tt = 0, hh = 0;
q[tt ++ ] = S;
dist[S] = 0;
st[S] = true; // 最好写上,好习惯
while (hh != tt)
{
int t = q[hh ++ ];
if (hh == N) hh = 0;
st[t] = false; // 退出队列后还有可能进来
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if (dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i];
if (!st[j]) // 保证队列中这个点只有一个
{
q[tt ++ ] = j;
if (tt == N) tt = 0;
st[j] = true;
}
}
}
}
return dist[T];
}
STL 队列
int spfa(int S, int T)
{
memset(dist, 0x3f, sizeof dist);
memset(st, 0, sizeof st);
queue<int> q;
q.push(S);
dist[S] = 0;
st[S] = true; // 这句话可有可无,毕竟入队之后直接就 st[t] = false; 了
while (q.size())
{
int t = q.front();
q.pop();
st[t] = false;
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if (dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i];
if (st[j] == 0)
{
st[j] = true;
q.push(j);
}
}
}
}
return dist[T];
}
扩展
- SPFA [[判断负环]]
- 求解最长路,有两种方案,第一种就是把边权变为相反数,跑一边最短路即可,注意答案还要再取一次相反数变回来。第二中就是按dist变大的方式进行正常spfa,这也是正确的。
更多的问题
基于的搜索版本
关于一般来说,任何迭代都是可以写成递归的形式,对于 SPFA,我们一般写 bfs 的版本,网上还有 dfs 的,但效率一般不如 bfs,常用于判断负环,但有一说一,如果把 bfs 版的SPFA 的队列换成栈,那么就相当 dfs 版本的 SPFA了,dfs 版可以学,但貌似没什么大用,就和堆优化的 prim 算法一样,有用,但没什么大用,可以被替代。
进队的意义
以下部分为错误证明,贵在思考,因此保留。
以下证明命题喂给 AI 成功生成错误证明,这也证明了不能轻信 AI。
队列里存的点最短路的经过边数一定是单调不减的(错误结论)
证明:一个经过 \(k\) 条边的点,一定要被经过 \(k - 1\) 条边的点更新,而只有更新才能引起入队。比如最开始起点更新其他点,这时边数为 \(1\),这些边数为 \(1\) 的点又可以更新出一堆边数为 \(2\) 的点,以此可知这是正确的。
注意,这里指的经过边数,是指在入队那一刻的经过边数,因为一个点即使加入了队列,也可能会被再次松弛,但此时无法加入队列(因为它已经存在)而经过边数确实是改变了,因此,我们说的是入队时的经过边数,而不是实时的边数。实时边数是会非单调的。即入队时的边数顺序是单调不减的。
以下内容存疑
再补充一点,队列中的点最短路的经过边数最多只有 \(2\) 种且相邻,因为经过 \(3\) 条边的点无法更新出经过 \(5\) 条边的点,因此最多只有两种。
下面是 hack 数据
9 9
4 8 1
5 9 1
3 7 1
2 4 1
1 6 3
1 5 3
1 4 3
1 3 3
1 2 1
错误的原因在于,spfa 中的连续更新
进入队列 \(n\) 次,最短路径至少经过 \(n\) 条边(正确的结论,但证明错误)
第一种思路(较麻烦):一个点 \(u\) 能入队,首先它要先被更新,并且不在队中,而能更新它的点 \(k\) 一定是之前已经被更新过的点(包括起点),设 \(k\) 最短路的经过的边数为 \(j\),那么 \(u\) 点更新的最短路径经过 \(j + 1\) 条边。对于 \(k\) 点,更新 \(k\) 使其入队的点 \(p\) 也是如此,但它的最短路经过的条数一定小于等于 \(j - 1\),因为在 \(k\) 入队和出队之间,\(k\) 可能还会被别的点更新,但此时 \(k\) 已经入队,不能再入。回头看看我们的队列,里面存的点的最短路径经过边数(入队时)一定是单调不减的,因此上面的别的点的最短路经过的边数一点大于等于 \(p\) 的。因此我们可以得出结论,如果一个点入队 \(n\) 次那么,它的最短路经至少经过 \(n\) 边。这里可以看Bellman_Ford 算法。
第二种思路(更好理解):已知队列里存的点的(入队时)最短路经过边的边数一定是单调不减的,如果一个点被更新入队了(此时边数至少为 \(1\),除了起点),且这个点在之后还能入队,那么它先要出队,而它出队之后因为边数单调不减,它只会被比它经过边数大或等于的点更新入队,而更新入队后边数至少加 \(1\)。第一次入队,边数至少为 \(1\),之后每次入队边数都会加 \(1\)。因此进入队列 \(n\) 次,最短路径至少经过 \(n\) 条边。这里说的更新包括最开始起点的入队初始化为 \(0\)。
由此可以推出一个结论:在 SPFA 中如果有任何一个点进队次数超过 \(n\) 则图上必定有负环。
或者,在 SPFA 中,在一个无负环图上,有任何一个点的进队次数都不超过 \(n - 1\) 次 。
对于以下有向图,源点为 \(1\),进行 spfa 输出进队时点的最短路的经过边数为 1 1 1 1 1 2 3 2 并不单调递减 图为:
4 8 1
5 9 1
3 7 1
2 4 1
1 6 3
1 5 3
1 4 3
1 3 3
1 2 1
图为:
![[Pasted image 20250804073503.png|250]]

浙公网安备 33010602011771号