SPFA -----队列优化的Bellman-Ford

发布时间 2024-01-01 15:52:38作者: 凪风sama

SPFA ------队列优化的Bellman-Ford

由Bellman-Ford算法实现带有负权边的单源最短路,时间复杂度是O(VE),也就是边数乘顶点数。但是根据Bellman-Ford的状态转移方程$$dist[i] = min(dist[i] , last[k] + w[k -> i])$$可知,当且仅当顶点k的dist值在上一层有更新时,在本层更新时顶点 i 的dist值才有可能变小。因此SPFA出现了

SPFA算法相较于Bellman-Ford多维护了一个队列,队列中存储可能使得最短路获得更新的顶点。每次更新我们仅使用队列中的顶点来进行更新,同时每次将更新过的顶点加入队列,这样就避免了无用的更新操作。

SPFA求带负权边的单源最短路

给出例题851. spfa求最短路 - AcWing题库

#include <iostream>
#include <cstring>
#include <vector>
#include <queue>
using namespace std;
const int N = 1e5+20;
int n,m;
vector<pair<int,int>> g[N];
int dist[N]; 
bool st[N]; //判断该顶点是否已经在队列中,防止重复入队
void spfa()
{
    memset(dist,0x3f,sizeof dist);
    dist[1] = 0; 
    queue<int> q;
    q.push(1); //将源点加入队列中
    st[1] = true;
    while(q.size())
    {
        int k = q.front();
        q.pop();
        st[k] = false; //出队后状态置为false
        for(auto x : g[k])
        {
	        int v = x.first, w = x.second;
            if(dist[v] > dist[k] + w)
            {
                dist[v] = dist[k] + w;
                if(!st[v]) //防止重复入队
               {
	                q.push(v);
	                st[v] = true;
                }
            }
        }
    }
    
}
int main()
{
    cin >> n >> m;
    while(m --)
    {
        int x,y,z;
        cin >> x >> y >> z;
        g[x].push_back({y,z});
    }
    spfa();
    if(dist[n] > 0x3f3f3f3f/2) puts("impossible");
    else cout << dist[n];
    return 0;
}
SPFA判断图中是否有负环

和Bellman-Ford一样,当图中出现负环时(如下图)

flowchart LR A((A)) -->|2| B((B)) B -->|-5| C((C)) C -->|1| A

根据从源点到目标点的最短路至多经过n-1条边,当存在负环时,更新操作可以一直进行,也就是说边数会大于n-1。我们只需要监视在更新过程中是否出现经过边数大于n-1即可。
如下题
852. spfa判断负环 - AcWing题库
代码如下

#include <iostream>
#include <cstring>
#include <vector>
#include <queue>
using namespace std;
const int N = 2500;
vector<pair<int,int>> g[N];
int dist[N];
int st[N],cnt[N];
int n,m;
bool spfa()
{
    queue<int> q;
    for(int i = 1 ; i <= n ; i++)
    {
        q.push(i);   
        st[i] = true;
    }
    while(q.size())
    {
        int k = q.front();
        q.pop();
        st[k] = false;
        for(auto x : g[k])
        {
            int v = x.first, w = x.second;
            if(dist[v] > dist[k] + w)
            {
                dist[v] = dist[k] + w;
                cnt[v] = cnt[k] + 1;
                if(cnt[v] >= n)
                    return true;
                if(!st[v])
                {
                    q.push(v);
                    st[v] = true;
                }
            }
        }
    }
    return false;
}
int main()
{
    cin >> n >>m;
    while(m --)
    {
        int x,y,z;
        cin >> x >> y >> z;
        g[x].push_back({y,z});
    }
    if(spfa())
        cout << "Yes";
    else
        cout << "No";
    return 0;
}

几点细节

  1. 这里开始将所有顶点都入队,是因为若仅使用一个源点的话,出现孤立的不连通块会使得我们在找负环或者说更新最短路时有疏漏。因此初始将所有顶点加入队列,且不用初始化dist数组。因为我们目标是找出负值圈,而如果有负值圈,最短路会小于0且会一直更新,也就是说队列永远不会空,且dist会一直减小。我们只需要在某个节点检测到cnt[v] >= n即可
  2. 以上将所有顶点入队的操作也可以理解为有一个虚拟的源点s,其与图中各个点都有一条权值为0的点,也就说该虚拟源点使得图联通起来。然后使用该虚拟源点更新就是将各个顶点加入队列。
  3. 关于cnt[v] = cnt[k] + 1。显然若k顶点能够更新dist[v],那么顶点v的最短路所经过的边数即为顶点k所经过的边数加上更新的这条边数。