算法学习笔记(27)——SPFA算法(单源最短路)

SPFA 算法

SPFA算法实际上是对Bellman-Ford算法的队列优化,也是用于在存在负权边的图上,求单源点最短路,一般情况下时间复杂度可以看作 \(O(m)\) ,最坏情况下时间复杂度是 \(O(nm)\)

虽然SPFA算法是对Bellman-Ford算法的优化,但是不是所有用Bellman-Ford算法的问题都能用SPFA来代替。例如,对最短路经过的边数做一个限制,要求经过的边数 \(\le k\) 的最短路,这个时候能用Bellman-Ford算法,但是不能用SPFA算法来做。

SPFA算法是单源最短路问题中限制最少的算法。在求单源点最短路的时候,只要图中没有负环就可以用SPFA算法,而且一般要求单源点最短路的问题都是没有负环的。只是如果没有负权边的话,用Dijkstra会更快,都是在有负权边的时候才用SPFA。

回顾Bellman-Ford算法,我们在 \(n\) 次循环中,每次都要遍历所有的 \(m\) 条边来做松弛操作更新dist[]数组,但实际上并不是每条边都会产生更新,SPFA正是对此进行了优化。在Bellam-Ford的松弛操作中,dist[b] = min(dist[b], last[a] + w),只有当last[a],即上一轮的dist[a]变小了,才会更新这一轮的dist[b],而ba的后继节点,所以只有前驱节点变小了,后继节点才会更新

SPFA运用宽搜做优化,维护一个队列,最开始将起点加入到队列中,作为更新的前驱。队列里存的始终是"变小了"的节点,只要队列不空,就取出队头节点,并利用此节点的边做更新,把这些边对应的后继节点入队。可以用伪代码表示:

queue <= 1
while queue不空
    t <= q.front
    q.pop
    更新t的所有出边 t =w=> b
    queue <= b

另外在加入队列之前可以再优化一下,如果队列里已经有 \(b\) 了,就不用再加入了,因为在未来的某个时刻一定会把 \(b\) 拿出来更新,这就足够了,不需要在意是因为结点 \(t_1\) 还是 \(t_2\) 的更新导致了 \(b\) 节点的变小。

SPFA 求最短路

题目链接:AcWing 851. spfa求最短路

注意SPFA相比Bellman-Ford算法的优化之后,每次是要拿出一个结点的所有后继边来尝试更新的,所以这个时候“结点后继”这个语义是需要的了,因此不能像Bellman-Ford算法那样直接把所有的边存到数组里去,这里直接存到邻接表里。

用STL的队列,不好判断一个结点是不是已经存在队列里了,这里的实现方式是用一个st数组来同步记录这个消息:如果一个结点t出队列了,就st[t] = false;如果一个结点b进入队列了,就st[b] = true

#include <iostream>
#include <queue>
#include <cstring>

using namespace std;

const int N = 1e5 + 10;

int n, m;
int h[N], e[N], ne[N], w[N], idx; //带权的邻接表存图
bool st[N];     // 用于入队优化,标记当前节点是否在队列中
int dist[N];    // 距离数组

// 邻接表加边
void add(int a, int b, int c)
{
    e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx ++;
}

void spfa()
{
    // 初始化距离数组
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    
    queue<int> q;
    // 初始阶段将起点入队
    q.push(1);
    st[1] = true;
    
    // 宽搜
    while (q.size()) {
        int t = q.front();
        q.pop();
        st[t] = false;
        
        // 枚举每一条节点t的出边
        for (int i = h[t]; i != -1; i = ne[i]) {
            int j = e[i];
            // 看看先到t再到j是不是更短,更短的话再更新
            if (dist[j] > dist[t] + w[i]) {
                dist[j] = dist[t] + w[i];
                // 如果j不在队列中,再入队,避免重复入队
                if (!st[j]) {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }
}

int main()
{
    // 初始化邻接表
    memset(h, -1, sizeof h);
    
    cin >> n >> m;
    for (int i = 0; i < m; i ++ ) {
        int x, y, z;
        cin >> x >> y >> z;
        add(x, y, z);
    }
    
    spfa();
    
    if (dist[n] > 0x3f3f3f3f / 2) puts("impossible");
    else cout << dist[n] << endl;
    
    return 0;
}

SPFA 判断负环

题目链接:AcWing 852. spfa判断负环

如果图里面存在负环,那么求单源最短路的过程会一直进行下去,每次走一圈负环路径长度都会变小。
SPFA算法中dist[j]记录了从起点到j的最短距离,我们再维护一个cnt[]数组,cnt[j]表示到j经过了多少步,如果达到了n步以上,这个路径一定有负环。

注意点:负环可能从任意一个点出发,所以初始化队列时将所有点入队

#include <iostream>
#include <cstring>
#include <queue>

using namespace std;

const int N = 2010, M = 10010;

int n, m;
int h[N], e[M], ne[M], w[M], idx; // 邻接表存储最多 N个点 M条边的图
int dist[N];
bool st[N];
int cnt[N];     // 记录到某一点经过了多少步

void add(int a, int b, int c)
{
    e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx ++;
}

bool spfa()
{
    // 由于所有点都要入队,所以dist的语义已经不是单源点的最短路长度了
    // 而且求的不再是距离的绝对值,这里就默认全是0也一样能判断负环
    // memset(dist, 0x3f, sizeof dist);
    // dist[1] = 0;
    
    // 初始化队列,将所有点都入队
    queue<int> q;
    for (int i = 1; i <= n; i ++ ) {
        q.push(i);
        st[i] = true;
    }
    
    // 宽搜套路
    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];
                // 更新了到j的最短路径 = 到t的最短路径加1
                cnt[j] = cnt[t] + 1;
                // 如果路径长度达到n(n个点不重复最多n-1条边),代表一定有负环
                if (cnt[j] == n) return true;
                if (!st[j]) {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }

    return false;
}

int main()
{
    memset(h, -1, sizeof h);
    
    cin >> n >> m;
    while (m -- ) {
        int x, y, z;
        cin >> x >> y >> z;
        add(x, y, z);
    }
    
    if (spfa()) puts("Yes");
    else puts("No");
    
    return 0;
}

特别注意在这种求任意点出发的负环里,dist[]已经只是作为一个“比较和更新”的工具来用了,它的值是多少不关心,它的语义也不再是从单源点到某个点的最短路长度了。由于不关心距离的绝对值,所以也没必要初始化为正无穷,只要保证里面的数初始都是一样的,这样就能正常的根据w来更新就行了

posted @ 2022-12-10 09:32  S!no  阅读(120)  评论(0编辑  收藏  举报