【算法磨剑:用 C++ 思考的艺术・单源最短路收官】BF/SPFA 负环判断模板 + 四大算法全总结
文章目录
- 前言:
- 《算法磨剑: 用C++思考的艺术》 专栏
- 《C++:从代码到机器》 专栏
- 《Linux系统探幽:从入门到内核》 专栏
- 正文:
- BF 算法判断负环
- 代码实现:
- spfa 算法判断负环
- 代码实现:
- 单源最短路算法总结:
- [P1629 邮递员送信](https://www.luogu.com.cn/problem/P1629) [P1744 采购特价商品](https://www.luogu.com.cn/problem/P1744) [P2136 拉近距离](https://www.luogu.com.cn/problem/P2136) [P1144 最短路计数](https://www.luogu.com.cn/problem/P1144)
- 结语:
前言:
《算法磨剑: 用C++思考的艺术》 专栏
专注用C++破解算法面试真题,详解LeetCode热题,构建算法思维,从容应对技术挑战。
👉 点击关注专栏
《C++:从代码到机器》 专栏
深入探索C++从语法特性至底层原理的奥秘,构建坚实而深入的系统编程知识体系。
👉 点击关注专栏
《Linux系统探幽:从入门到内核》 专栏
深入Linux操作系统腹地,从命令、脚本到系统编程,探索Linux的运作奥秘。
👉 点击关注专栏
作者:孤廖
学习方向:C++/Linux/算法
人生格言:折而不挠,中不为下
正文:
P3385 【模板】负环
借助该题 讲述bf算法,spfa算法对带有负环的有向图的单源最短路问题模板的讲述
【解法】
如果图中存在负环,那么有可能不存在最短路。
因为 会在负环中一直走下去,找不到最小路。
BF 算法判断负环
- 执⾏ n 轮松弛操作,如果第 n 轮还存在松弛操作,那么就有负环
解释:如果图中存在负环则会一直进行松弛操作 ,这里我们只需判断第n轮是否进行松弛操作即可
代码实现:
#define _CRT_SECURE_NO_WARNINGS
//bf判断负环模板#include <iostream>
#include <cstring>
using namespace std;
const int M = 3e3 + 10,N=2e3+10;
int T, n, m;//多组数据T ,点数,边数
struct edges
{int u, v, z;//u~v 的权值z
}e[M*2];
int dist[N];//dist[i]:节点i 距离起始点1的最小路径
int pos;//实际边的条数
bool bf()
{//初始化memset(dist, 0x3f3f3f3f, sizeof dist);dist[1] = 0;//bf算法bool falg;//标记此轮是否有松弛操作for (int i = 1; i <= n; i++)//如果有环松弛的轮数超过n-1{falg = false;//遍历所有边for (int i = 1; i <= pos; i++){int u = e[i].u, v = e[i].v, z = e[i].z;if (dist[u] == 0x3f3f3f3f) continue;//前面的点还没松弛到u//判断是否进行松弛操作if (dist[u] + z < dist[v]){//松弛操作dist[v] = dist[u] + z;falg = true;}}if (!falg) return falg;//当前轮没有松弛操作 说明所有点 //单源最小路径已经确定 没有负环}return falg;//第n轮仍然进行松弛操作 说明有负环
}
int main()
{cin >> T;while (T--){pos = 0;//处理多组数据,数据覆盖清楚上一组数据//处理边的信息cin >> n >> m;for (int i = 1; i <= m; i++){int u, v, z;cin >> u >> v >> z;pos++;e[pos].u = u, e[pos].v = v, e[pos].z = z;//u~v//判断是否为双向边if (z >= 0){pos++;e[pos].u = v, e[pos].v = u, e[pos].z = z;//v~u}}//输出结果if (bf()) cout << "YES" << endl;else cout << "NO" << endl;}return 0;
}
spfa 算法判断负环
维护⼀个 cnt 数组记录从起点到该点所经过的边数,如果 cnt[i] >= n ,说明有负环。
解释:spfa算法是运用stl中的队列对bf算法的选边操作进行的优化。在代码实现上每次从队列中拿取之前松弛过的边 如果存在负环话 队列始终有元素 会死循环 因此相对bf算法通过控制循环次数来判断是否有负环,spfa可以采用cnt数组记录每个点到起始点的路径上经过的边数来判断是否有负环
代码实现:
#define _CRT_SECURE_NO_WARNINGS
//spfa 判断负环模板
#include <iostream>
#include <cstring>
#include <queue>
#include <vector>
using namespace std;
const int N=2e3+10;
typedef pair<int, int> PII;
vector<PII> edges[N];
int T, n, m;//多组数据T ,点数,边数
int dist[N];//dist[i]:节点i 距离起始点1的最小路径
bool st[N];//st[i]:节点i是否被松驰过已经放在队列里 避免重复对一个点松弛
//因为spfa算法用的队列对拿到松弛边的优化 如果存在负环的话 就死循环了
//所以用cnt数组来判断 每个点到达起源点的边数是否超过不存在
//负环情况下的最大边数n-1
int cnt[N];//cnt[i]:表示起始点1到点i经历的边数
bool spfa()
{//初始化//多组数据记得清空上一组数据memset(dist, 0x3f3f3f3f, sizeof dist);memset(st, 0, sizeof st);memset(cnt, 0, sizeof cnt);dist[1] = 0;queue<int> q;q.push(1);//源点入队st[1] = true;//遍历所有被松弛过的点边while (q.size()){//拿到边的信息int u = q.front(); q.pop();st[u] = false;//出队for (auto& e : edges[u]){int v = e.first, z = e.second;//判断是否松弛if (dist[u] + z < dist[v]){//松弛dist[v] = dist[u] + z;cnt[v] = cnt[u] + 1;//dp//判断是否有环if (cnt[v] >= n) return true;//有负环//判断是否入队 避免对一个点的出边进行重复松弛if (!st[v]){q.push(v);st[v] = true;}}}}return false;//无负环
}int main()
{cin >> T;while (T--){cin >> n >> m;//多组数据 清空当前组所需空间的数据for (int i = 1; i <= n; i++) edges[i].clear();//存储边的信息for (int i = 1; i <= m; i++){int u, v, z;cin >> u >> v >> z;//u~vedges[u].push_back({ v,z });//判断是否有双边if (z >= 0) edges[v].push_back({ u,z });//v~u}//输出结果if (spfa() ) cout << "YES" << endl;else cout << "NO" << endl;}return 0;
}
单源最短路算法总结:
注意注意注意:
所有的模板学完之后,希望⼤家先不要着急去刷题,⽽是花上⼀段时间消化⼀下上述所有的最短路算法。
- 重点不是去记忆代码,⽽是去记忆每⼀个最短路算法的流程。把纸和笔拿出来,⾃⼰给⾃⼰讲⼀遍
某个最短路算法。⼀定要把基础打好再去刷题- 不要偷懒,只想着记住⼀个就完事了。题⽬是灵活的,我们如果掌握多个最短路算法,也是可以灵活应对的。
常规版 - dijkstra | 堆优化 - dijkstra | bellman - ford 算法 | spfa 算法 | |
---|---|---|---|---|
算法思想 | 贪心 - 每次拿出还未确定最短路的点中,距离起点最近的点; - 打上标记之后,更新出边所连点的最短路。 | 使用堆优化找点操作: - 把还未确定最短路的点扔到堆中,用堆快速找出距离起点最近的点。 | 暴力松弛: - 执行 n - 1 轮松弛操作; - 每次都扫描所有的边,看看能否松弛。 | 使用队列优化 bf 算法: - 只有上一轮被松弛的点,下一轮才有可能松弛。 |
负边权 | 失效 | 失效 | 可行 | 可行 |
负环 | 失效 | 失效 | 可以判断负环: - 执行 n 轮操作,判断是否松弛 | 可以判断负环: - 创建 cnt 数组,标记从起点到该点的边数 |
时间复杂度 | O(n2)O(n^2)O(n2) | O(mlogm)O(m\log m)O(mlogm) | O(nm)O(nm)O(nm) | O(km)∼O(nm)O(km) \sim O(nm)O(km)∼O(nm) |
其实还有两个单源最短路算法,那就是普通 bfs 以及 01bfs:
• 普通 bfs 只能处理边权全部相同且⾮负的最短路;
• 01bfs 只能解决边权要么为 0,要么为 1 的情况
大家只要将上述四种算法流程熟记于心,吃透模板,那么大多数的单源最短路问题都可以解决,不同问题代码要会灵活改动,具体问题具体分析~
以下是我为大家找到的四道应用题,在吃透模板后可以自己加以练习,本文就不在此些问题做代码实现了。但是如果想要交流的话可以私信。
P1629 邮递员送信
P1744 采购特价商品
P2136 拉近距离
P1144 最短路计数
结语:
这篇 “算法磨剑”,咱们终于给单源最短路系列画上了句号 —— 不仅补全了 BF 和 SPFA 的 “负环判断” 模板(BF 靠 “n 轮后仍能松弛”、SPFA 凭 “节点入队超 n 次”,两种思路各有适配场景),还把 Dijkstra(朴素 / 堆优化)、BF、SPFA 这四大算法的适用场景、时间复杂度、核心优劣做了系统总结,形成完整的 “算法工具箱”。
从 “非负权图用 Dijkstra” 到 “负权边靠 BF/SPFA”,再到 “负环检测补全边界场景”,整个系列其实在讲一个逻辑:算法选择的本质是 “问题场景与效率的匹配”。比如稀疏图选堆优化 Dijkstra,含负权但无负环用 SPFA,需限制路径边数就用 BF—— 吃透这点,面对不同题目才能快速 “对号入座”。
如果这篇总结帮你理清了单源最短路的知识脉络,不妨点个赞 + 收藏,方便后续刷题时快速查阅;也欢迎在评论区交流:你觉得哪个算法的负环判断逻辑最容易记?刷题时有没有遇到过 “四大算法都能解,但效率天差地别” 的题目?接下来专栏会转向 “多源最短路” 或 “最小生成树进阶”,咱们继续在算法的世界里 “磨剑”~