SPFA算法详解

前置知识:Bellman-Ford算法

前排提示:SPFA算法非常容易被卡出翔。所以如果不是图中有负权边,尽量使用Dijkstra!(Dijkstra算法不能能处理负权边,但SPFA能)

前排提示*2:一定要先学Bellman-Ford!

0.引子

在Bellman-Ford算法中,每条边都要松弛\(n-1\)轮,还不一定能松弛成功,这实在是太浪费了。能不能想办法改进呢?

非常幸运,SPFA算法能做到这点。(SPFA又名“Bellman-Ford的队列优化”,就是这个原因。)

1.基本思想

先说一个结论:只有一个点在上一轮被松弛成功时,这一轮从这个点连出的点才有可能被成功松弛。

为什么?显而易见

好吧其实我当初也花了不少时间理解这玩意

松弛的本质其实是通过一个点中转来缩短距离(如果你看了前置很容易理解)。所以,如果起点到一个点的距离因为某种原因变小了,从起点到这个距离变小的点连出的点的距离也有可能变小(因为可以通过变小的点中转)。(通读三遍再往下看)

所以,可以在下一轮只用这一轮松弛成功的点进行松弛,这就是SPFA的基本思想。

2.用队列实现

我们知道了在下一轮只用这一轮松弛成功的点进行松弛,就可以把这一轮松弛成功的点放进队列里,下一轮只用从队列里取出的点进行松弛。

为什么是队列而不是其他的玄学数据结构?因为队列具有“先进先出,后进后出”的特点,可以保证这一轮松弛的点不会在这一轮结束之前取出。

干说可能不太理解,所以还是举个栗子吧。

这又是之前的有向图,但是这次我们要用SPFA跑。

最开始,我们要把\(1\)号点放进队列(为什么要这样?先往下看)。\(dis\)数组和队列是这个亚子的:

\(i\)\(dis[i]\)
\(1\)\(0\)
\(2\)\(\infty\)
\(3\)\(\infty\)
\(4\)\(\infty\)
queue1

\(1\)号点进行松弛(就是\(1\)号到\(1\)号再到目标点):

\(i\)\(dis[i]\)
\(1\)\(0\)
\(2\)\(1\)
\(3\)\(5\)
\(4\)\(\infty\)
queue23

\(2,3\)号点被松弛成功了,把它们加入到队列里。

\(1\)号点被用过了,把它扔掉。(工具点石锤)

\(2\)号点进行松弛:

\(i\)\(dis[i]\)
\(1\)\(0\)
\(2\)\(1\)
\(3\)\(5\)
\(4\)\(3\)
queue34

\(4\)号点被松弛成功了,把它们加入到队列里。

\(2\)号点被用过了,把它扔掉。

\(3\)号点进行松弛:

\(i\)\(dis[i]\)
\(1\)\(0\)
\(2\)\(1\)
\(3\)\(5\)
\(4\)\(3\)
queue4

没有点被松弛成功。

\(3\)号点被用过了,把它扔掉。

\(4\)号点进行松弛:

\(i\)\(dis[i]\)
\(1\)\(0\)
\(2\)\(1\)
\(3\)\(4\)
\(4\)\(3\)
queue3

\(3\)号点被松弛成功了,把它们加入到队列里。

\(4\)号点被用过了,把它扔掉。

\(3\)号点进行松弛:

\(i\)\(dis[i]\)
\(1\)\(0\)
\(2\)\(1\)
\(3\)\(4\)
\(4\)\(3\)
queue

没有点被松弛成功。

\(3\)号点被用过了,把它扔掉。

现在队列为空(也就是能松弛的都松弛了),算法结束。

3.Code

SPFA的具体实现,推荐结合上面的栗子食用。

#include <bits/stdc++.h>
using namespace std;
#define MAXN 10005
#define INF 0x7fffffff
int n,m,s,dis[MAXN];
vector<pair<int,int> > g[MAXN];//用vector存图,但是据说链式前向星更快
void spfa(){
    queue<int> q;
    q.push(s);//把初始点加入队列
    fill(dis+1,dis+1+n,INF);//因为一开始所有点都到不了,所以初始化为INF
    dis[s]=0;//自己到自己肯定距离为0
    while(!q.empty()){
        int u=q.front();
        q.pop();//从队列里取出第一个元素
        for(int i=0;i<g[u].size();i++){
            int v=g[u][i].first,w=g[u][i].second;
            if(dis[v]>dis[u]+w){
                dis[v]=dis[u]+w;
                q.push(v);
            }
            //如果能松弛成功,那么松弛,把松弛成功的目标点放入队列
        }
    }
}
int main(){
    scanf("%d%d%d",&n,&m,&s);
    for(int i=1;i<=m;i++){
        int u,v,w;
        scanf("%d%d%d",&u,&v,&w);
        g[u].push_back(make_pair(v,w));
    }//输入,建图
    spfa();
    for(int i=1;i<=n;i++){
        printf("%d ",dis[i]);
    }//输出
    return 0;
}

这个代码能够ACP3371,但是我还是推荐你自己码一遍。

4.特点

  • 能够处理有负权边的图,但是隔壁Dijkstra不行。
  • 在有负环的情况下,不存在最短路,因为不停在负环上绕就能把最短路刷到任意低。但是SPFA能够判断图中是否存在负环,具体方法为统计每个点的入队次数,如果有一个点入队次数$ \ge n $,那么图上存在负环,也就不存在最短路了。
  • 什么?你不知道什么叫负环?下面的就是。

就是一个环,边权和是负。一般用一个名为菜鸡算法的算法判断

——Ynoi

  • SPFA的时间复杂度是\(O(km)\)\(k\)是每一个节点的平均入队次数,经过实践,\(k\)一般为\(4\),所以SPFA通常情况下非常高效。但是SPFA非常容易被卡出翔,最坏情况下会变成\(O(nm)\) 所以如果能用隔壁Dijkstra尽量不要用SPFA。至于具体怎么卡,据说是这样的:

(这种图据说叫菊花图,能欺骗SPFA多次让点进入队列,所以\(k\)会变得非常大(上限为\(n\))。)

5.你都看到这了就不点一个赞吗?

这个最重要了qwq

### SPFA算法简介 SPFA(Shortest Path Faster Algorithm)是一种用于解决带有负权边的单源最短路径问题的算法[^2]。该算法是对经典的Bellman-Ford算法的一种优化,通过引入队列机制减少冗余计算,提升了处理效率。 #### 特点与适用范围 作为一种高效的最短路径查找方法,SPFA尤其适合于存在负权重边的情况,并能够有效检测是否存在负权回路[^1]。对于稀疏图而言,其实际性能往往优于传统的Dijkstra算法[^4]。 ### 工作原理概述 核心思想在于利用先进先出(FIFO)队列存储待更新距离信息的节点集合。每当某个节点的距离被成功缩短时,则将其相邻节点加入队列等待进一步探索。此过程持续至无任何可改善为止。 具体流程如下: - 初始化起点到各目标结点间的初始预估值; - 将起始位置压入队列开始迭代; - 对当前访问元素关联的所有邻接关系实施松弛操作; - 若某次调整导致新发现更优路线且对应终点尚未处于排队状态,则追加进列表继续考察直至收敛完成。 ### 时间复杂度分析 理论上讲,在极端条件下(即所有可能情形均需经历完整轮询),SPFA的时间消耗会退化成同等于原始版本O(n*m)[^3];不过实践中多数时候表现得更好些,大约维持在线性级别左右O(k*m),这里k代表一个小得多的比例因子。 ### 实现样例 以下是Python语言下的简单实现方式: ```python from collections import deque, defaultdict def spfa(graph, start_node): dist = {node: float('inf') for node in graph} dist[start_node] = 0 queue = deque([start_node]) while queue: current_node = queue.popleft() for neighbor, weight in graph[current_node]: potential_new_dist = dist[current_node] + weight if potential_new_dist < dist[neighbor]: dist[neighbor] = potential_new_dist if neighbor not in queue: queue.append(neighbor) return dist ``` 上述代码片段定义了一个名为`spfa`的功能函数接收两个参数:一个是描述网络结构的数据字典形式graph;另一个是指定出发端口编号start_node。最终返回值dist记录着由指定根部抵达各个分支末端所需的最小累计开销。
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值