SPFA最短路径算法(c++)

哈喽大家好!今天我们来详细吃透图论中SPFA最短路径算法

在图论最短路径问题中,我们最常用的两个算法:

  • Dijkstra :效率极高,但无法处理负权边,更无法处理负环

  • Bellman-Ford:可以处理负权边、判定负环,但时间复杂度极高 O(nm),大数据直接超时

SPFA(Shortest Path Faster Algorithm) 就是 Bellman-Ford 的队列优化版本 ,完美兼顾二者优势:支持负权边、能判负环、效率远高于朴素BF,是竞赛、刷题中处理负权最短路的首选算法。

本文将从零讲解SPFA原理、执行流程、手写C++模板、负环判定、常用优化、常见坑点,看完直接掌握可直接套用的工业级代码!

一、SPFA 前置知识:为什么需要它?

1.1 朴素 Bellman-Ford 的弊端

Bellman-Ford 的核心思路是:对所有边进行 n-1 轮松弛操作(n 为节点数),保证得到单源最短路径。

但它有一个致命问题:每一轮都会无脑遍历所有边,但大部分节点的距离根本没有更新,属于无效遍历,极大浪费时间。

1.2 SPFA 的核心优化思想

SPFA 基于一个关键结论:只有被更新过距离的节点,它的出边才有可能松弛其他节点

所以 SPFA 放弃了暴力遍历所有边,改用队列存储所有「距离被更新、需要松弛邻边」的节点,只处理有效节点,大幅减少无效计算,效率飙升。

1.3 算法适用场景

  • 求解带负权边的单源最短路径问题

  • 判定图中是否存在负权环(负环)

  • 稠密图、稀疏图均可,稀疏图效率最优(平均复杂度接近线性)

⚠️ 注意:若图存在负环,源点可达负环时,最短路径不存在(可以无限松弛变短)

二、核心概念:松弛操作

松弛操作是所有最短路算法的核心,SPFA 也不例外。

对于一条边 (u,v,w)(从u到v,权值为w):

如果满足:dist\[v\] \> dist\[u\] + w

说明:从源点经过 u 走到 v 的路径,比当前记录的 v 的最短路径更短。

此时更新:dist\[v\] = dist\[u\] + w,并将 v 加入队列,用于后续松弛其邻边。

三、SPFA 完整执行流程

我们以邻接表存储图(最常用、效率最高)为例,梳理完整步骤:

步骤1:初始化

  • 距离数组 dist[] 全部赋值为无穷大 INF

  • 源点距离置0:dist[s] = 0

  • 队列清空,源点入队

  • 标记数组 vis[] 记录节点是否在队列中(防止重复入队)

步骤2:循环松弛

  1. 取出队首节点 u,标记出队

  2. 遍历 u 的所有邻边 (u, v, w)

  3. 若可以松弛(更短路径),更新 dist[v]

  4. v 不在队列中,将其入队,等待后续松弛

步骤3:终止条件

队列为空时,所有可松弛的路径全部更新完毕,算法结束,dist[] 即为源点到各点的最短路径。

四、C++ 完整基础代码实现(最短路模板)

这里提供竞赛通用模板,采用链式前向星存图,代码简洁、高效、可直接套用,带详细注释。

复制代码
cpp 复制代码
#include <iostream>
#include <queue>
#include <cstring>
using namespace std;

const int N = 100010;  // 最大节点数
const int M = 200010;  // 最大边数
const int INF = 0x3f3f3f3f;

// 链式前向星存图
int h[N], e[M], w[M], ne[M], idx;
int dist[N];  // 存储源点到各点最短距离
bool vis[N];  // 标记节点是否在队列中

// 加边函数:a->b 权值c
void add(int a, int b, int c)
{
    e[idx] = b;
    w[idx] = c;
    ne[idx] = h[a];
    h[a] = idx++;
}

// SPFA核心函数:s为源点
void spfa(int s)
{
    // 初始化
    memset(dist, 0x3f, sizeof dist);
    memset(vis, false, sizeof vis);
    queue<int> q;

    dist[s] = 0;
    q.push(s);
    vis[s] = true;

    while (!q.empty())
    {
        int u = q.front();
        q.pop();
        vis[u] = false;  // 出队,取消标记

        // 遍历所有邻边
        for (int i = h[u]; i != -1; i = ne[i])
        {
            int v = e[i];
            int weight = w[i];

            // 松弛操作
            if (dist[v] > dist[u] + weight)
            {
                dist[v] = dist[u] + weight;
                // 不在队列则入队
                if (!vis[v])
                {
                    q.push(v);
                    vis[v] = true;
                }
            }
        }
    }
}

int main()
{
    // 初始化表头
    memset(h, -1, sizeof h);
    int n, m;
    cin >> n >> m;

    // 读入m条边
    for (int i = 0; i < m; i++)
    {
        int a, b, c;
        cin >> a >> b >> c;
        add(a, b, c);
    }

    // 以1号节点为源点跑最短路
    spfa(1);

    // 输出结果
    for (int i = 1; i <= n; i++)
    {
        if (dist[i] == INF) cout << "无法到达 ";
        else cout << dist[i] << " ";
    }
    return 0;
}

代码关键点解析

  1. vis数组的作用:避免同一个节点重复入队,防止队列冗余、超时

  2. 0x3f3f3f3f:常用无穷大值,相加不会溢出,适配int范围

  3. 链式前向星:相比vector邻接表,速度更快,适配大数据图论题目

五、SPFA 进阶:负权环判定

SPFA 最核心的进阶功能就是判断图中是否存在负环,这是Dijkstra无法做到的。

5.1 判定原理

根据图论定理:n-1一个不含负环的图,任意节点的最短路径最多经过 条边(n为节点数)

如果某个节点n-1入队次数超过 次 ,说明该节点被无限松弛,图中存在源点可达的负环

5.2 负环判定完整代码

cpp 复制代码
int cnt[N];  // 记录每个节点入队次数

// 返回true表示存在负环
bool spfa_check(int s, int n)
{
    memset(dist, 0x3f, sizeof dist);
    memset(vis, false, sizeof vis);
    memset(cnt, 0, sizeof cnt);
    queue<int> q;

    dist[s] = 0;
    q.push(s);
    vis[s] = true;
    cnt[s] = 1;

    while (!q.empty())
    {
        int u = q.front();
        q.pop();
        vis[u] = false;

        for (int i = h[u]; i != -1; i = ne[i])
        {
            int v = e[i];
            int weight = w[i];

            if (dist[v] > dist[u] + weight)
            {
                dist[v] = dist[u] + weight;
                if (!vis[v])
                {
                    q.push(v);
                    vis[v] = true;
                    cnt[v]++;
                    // 入队次数超过n-1,存在负环
                    if (cnt[v] > n) return true;
                }
            }
        }
    }
    return false;
}

5.3 全域负环判定技巧

如果需要判断整张图是否存在负环(不限源点),无需枚举所有源点,只需:

  • 新建一个超级源点 0

  • 向所有节点连一条权值为0的边

  • 以0为源点跑SPFA,即可检测全图负环

六、时间复杂度与性能分析

6.1 复杂度

  • 平均时间复杂度(m为边数),效率极高,日常刷题完全够用

  • 最坏时间复杂度(极端卡数据场景,退化为朴素BF)

6.2 常用优化(防卡常)

竞赛中部分题目会卡普通SPFA,可加入两种经典优化,通过率拉满:

  1. SLF优化(小根队列):用双端队列,新节点距离小于队首则插入队首,否则插队尾,减少松弛次数

  2. LLL优化:维护队列平均距离,优先松弛距离更小的节点

日常刷题SLF优化足够用,实现简单、提升显著。

七、新手常见坑点(重点避坑)

  • 坑1:忘记vis数组标记:导致节点重复入队,队列爆炸超时

  • 坑2:INF取值不当 :INF过大会溢出,过小会被误松弛,推荐 0x3f3f3f3f

  • 坑3:负环判断条件写错 :判定条件是 cnt[v] > n,不是 n-1

  • 坑4:无清空数组:多组测试数据时,未清空h、dist、vis数组,导致数据污染

  • 坑5:混淆有向图/无向图:无向图需要双向加边,有向图只需单向加边

八、SPFA vs Dijkstra vs Bellman-Ford 对比

算法 能否处理负权 能否判负环 平均复杂度 适用场景
Dijkstra(讲过了自己看哦) 正权图最短路
Bellman-Ford(下一篇博客讲解) 小规模负权图
SPFA 负权图、判负环(通用首选)

九、总结

  1. SPFA 是 Bellman-Ford 的队列优化版,核心是只松弛有效更新节点,规避无效计算;

  2. 核心能力:处理负权边、判定负环,是正权图下Dijkstra的完美补充;

  3. 代码模板可直接套用,日常刷题、竞赛、面试均高频考察;

  4. 存在极端卡数据场景,可搭配SLF优化进一步提升稳定性。

后续遇到负权最短路、负环判定问题,直接锁死 SPFA 即可!

点个关注再走

相关推荐
weixin_446260851 小时前
HANDOFF:基于蒸馏互补教师的人形机器人任务空间整体控制
人工智能·算法·机器人
c238561 小时前
C++11final与override6、智能指针
开发语言·c++
Java_2017_csdn1 小时前
在 Java 中,MessageFormat.format() 和 String.format() 函数对比?
java·开发语言·前端·数据库
噢,我明白了2 小时前
MyBatis-Plus 中IPage的分页查询
java·mybatis
商业模式源码开发2 小时前
知识付费推三返一模式详解:规则设计、分红算法与合规架构
算法·架构·推三返一
fengfuyao9852 小时前
基于MATLAB的HHT变换完整实现(含EMD分解与三维时频谱生成)
开发语言·算法·matlab
剑挑星河月2 小时前
98.验证二叉搜索树
java·算法·leetcode
kupeThinkPoem2 小时前
c++是否会读到部分写入的数据?
c++
罗超驿2 小时前
16.滑动窗口经典例题:最小覆盖子串(LeetCode 76)算法原理剖析
算法·leetcode·职场和发展