【算法磨剑:用 C++ 思考的艺术・单源最短路进阶】Bellman-Ford 与 SPFA 算法模板精讲,突破负权边场景
文章目录
- 前言:
- 《算法磨剑: 用C++思考的艺术》 专栏
- 《C++:从代码到机器》 专栏
- 《Linux系统探幽:从入门到内核》 专栏
- 正文:
- 一:bellman-ford 算法
- 介绍:
- Bellman‒Ford 算法流程:
- 代码实现:
- 二: spfa 算法
- 介绍:
- spfa 算法流程:
- 代码实现:
- 结语:
前言:
《算法磨剑: 用C++思考的艺术》 专栏
专注用C++破解算法面试真题,详解LeetCode热题,构建算法思维,从容应对技术挑战。
👉 点击关注专栏
《C++:从代码到机器》 专栏
深入探索C++从语法特性至底层原理的奥秘,构建坚实而深入的系统编程知识体系。
👉 点击关注专栏
《Linux系统探幽:从入门到内核》 专栏
深入Linux操作系统腹地,从命令、脚本到系统编程,探索Linux的运作奥秘。
👉 点击关注专栏
作者:孤廖
学习方向:C++/Linux/算法
人生格言:折而不挠,中不为下
正文:
继上篇 Dijkstra 算法后,本文带来能处理负权边的 Bellman - Ford 与优化版 SPFA 算法模板精讲~需注意:负环的判断技巧,我们留到下一篇博客详细讲解。
一:bellman-ford 算法
介绍:
Bellman‒Ford 算法(之后简称 BF 算法)是⼀种基于松弛操作的最短路算法,可以求出有负权的图的最短路,并可以对最短路不存在的情况进⾏判断
算法核⼼思想:不断尝试对图上每⼀条边进⾏松弛,直到所有的点都⽆法松弛为⽌。
Bellman‒Ford 算法流程:
- 准备⼯作:
- 创建⼀个⻓度为 n 的 dist 数组,其中 dist[i] 表⽰从起点到 i 结点的最短路
- 初始化: dist[1] = 0 ,其余结点的 dist 值为⽆穷⼤,表⽰还没有找到最短路。
- 重复:每次都对所有的边进⾏⼀次松弛操作
- 重复上述操作,直到所有边都不需要松弛操作为⽌。
最多重复多少轮松弛操作?
- 在最短路存在的情况下,由于⼀次松弛操作会使最短路的边数⾄少增加 1,⽽最短路的边数最多为 n -1 因此整个算法最多执⾏轮松弛操作 n - 1 轮。故总时间复杂度为 O(nm) 。
代码实现:
#define _CRT_SECURE_NO_WARNINGS
//bellman-ford 算法#include <iostream>
#include <vector>
using namespace std;
const int N = 1e4 + 10, INF = 2147483647;
typedef pair<int, int> PII;
vector<PII> edges[N];//edges[i]:i的出边点 和其对应的边权值
int n,m,s;//点,边的个数,起源点
int dist[N];//dist[i]:节点i距离起源点的最短路径
void bellman_ford()
{//初始化for (int i = 0; i <= n; i++) dist[i] = INF;dist[s] = 0;//bfbool flag = false;//标记该轮是否进行过松弛操作for (int i = 1; i < n; i++)//每次松弛一个边 最多n-1次{flag = false;//遍历所有点for (int i = 1; i <= n; i++){if (dist[i] == INF) continue;//避免INF 溢出 成为负数//将所有点的出边进行松弛操作for (auto& e : edges[i]){int v = e.first, z = e.second;if (dist[i] + z < dist[v]){dist[v] = dist[i] + z;flag = true;}}}if (flag == false) break;}//输出结果for (int i = 1; i <= n; i++){cout << dist[i] << " ";}
}
二: spfa 算法
介绍:
spfa 即 Shortest Path Faster Algorithm,本质是⽤队列对 BF 算法做优化。
在 BF 算法中,很多时候我们并不需要那么多⽆⽤的松弛操作:
- 只有上⼀次被松弛的结点,它的出边,才有可能引起下⼀次的松弛操作;
- 因此,如果⽤队列来维护"哪些结点可能会引起松弛操作",就能只访问必要的边了,时间复杂度就能降低
spfa 算法流程:
- 准备⼯作:
- 创建⼀个⻓度为 n 的 dist 数组,其中 dist[i] 表⽰从起点到 i 结点的最短路;
- 创建⼀个⻓度为 n 的 bool 数组 st ,其中 st[i] 表⽰ i 点是否已经在队列中。
- 初始化:标记 dist[1] = 0 ,同时 1 ⼊队;其余结点的 dist 值为⽆穷⼤,表⽰还没有找到最短路.
- 重复:每次拿出队头元素 u ,去掉在队列中的标记,同时对 u 所有相连的点 v 进⾏松弛操作。如果结点 v 被松弛,那就放进队列中。
- 重复上述操作,直到队列中没有结点为⽌
注意注意注意:
虽然在⼤多数情况下 spfa 跑得很快(最优时间复杂度可以到o(Km))(K是一个常数),但其最坏情况下的时间复杂度为 O(nm)。将其卡到这个复杂度也是不难的,所以在没有负权边时最好使⽤ Dijkstra 算法。如何优雅的卡spfa 感兴趣的可以看下。
代码实现:
#define _CRT_SECURE_NO_WARNINGS
//spfa 算法 本质是用队列对bellman-ford算法的优化#include <iostream>
#include <vector>
#include <queue>
using namespace std;
const int N = 1e4 + 10, INF = 2147483647;
typedef pair<int, int> PII;
vector<PII> edges[N];//edges[i]:i的出边点 和其对应的边权值
int n, m, s;//点,边的个数,起源点
int dist[N];//dist[i]:节点i距离起源点的最短路径
bool st[N];//st[i]:节点i 是否在队列中
void spfa()
{//初始化for (int i = 0; i <= n; i++) dist[i] = INF;dist[s] = 0;queue<int> q;//将上一轮进行过松弛操作的点加入队列中q.push(s);st[s] = true;//spafwhile (q.size()){int u = q.front();q.pop();st[u] = false;//将 u 的出边进行松弛操作for (auto& e : edges[u]){int v = e.first;int z = e.second;if (dist[u] + z < dist[v]){dist[v] = dist[u] + z;if (!st[v]) q.push(v);}}}//输出结果for (int i = 1; i <= n; i++) cout << dist[i] << " ";}
int main()
{cin >> n >> m >> s;for (int i = 1; i <= m; i++){int u, v, z;cin >> u >> v >> z;edges[u].push_back({ v,z });}spfa();return 0;
}
结语:
这篇 “算法磨剑:用 C++ 思考的艺术”,我们把Bellman-Ford和它的 “优化形态”SPFA在 “负权边场景” 里彻底 “磨” 通了 ——Bellman-Ford 靠 “k 轮全边松弛”,天生能处理含负权的图(还能限制最短路最多经过 k 条边),用 C++ 的memcpy做 “备份数组”,让 “松弛的顺序干扰” 变得可控;SPFA 则用队列优化,只对 “可能被更新的节点” 重复松弛,靠 C++ 的queue把时间复杂度从 “稳定 O (nm)” 优化到 “多数情况接近 O (m)”,完美适配 “非极端负权图” 的实战。
但这俩算法的故事还没结束 —— 它们能处理负权边,却也天生和 “负环”(能无限缩短路径的环)深度绑定:Bellman-Ford 能通过 “是否还能松弛” 判断负环存在,SPFA 更能高效定位负环。明天的专栏,我们就聚焦 “负环判断”,用具体例题看这俩算法如何从 “处理负权” 进阶到 “检测致命负环”,补上单源最短路的最后一块拼图。
如果今天的两个算法模板解析,帮你突破了 “Dijkstra 处理不了负权” 的瓶颈,不妨点个赞 + 收藏方便复习;也欢迎评论区交流:你学 Bellman-Ford 时,有没有被 “k 轮松弛” 的逻辑绕晕?用 SPFA 时,有没有遇到过 “队列优化反而变慢” 的极端场景?带着对 “负环” 的好奇,我们明天继续 “磨” 单源最短路的收尾技巧~