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];
}

扩展

  1. SPFA [[判断负环]]
  2. 求解最长路,有两种方案,第一种就是把边权变为相反数,跑一边最短路即可,注意答案还要再取一次相反数变回来。第二中就是按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]]

posted @ 2024-05-12 21:02  blind5883  阅读(31)  评论(0)    收藏  举报