代码随想录算法训练营 Day60 图论Ⅹ Bellmen_ford 系列算法

article/2025/7/25 10:25:26

图论

题目

94. 城市间货物运输 I
Bellmen_ford 队列优化算法 SPFA
大家可以发现 Bellman_ford 算法每次松弛 都是对所有边进行松弛。
但真正有效的松弛,是基于已经计算过的节点在做的松弛。
本图中,对所有边进行松弛,真正有效的松弛,只有松弛边(节点1->节点2) 和边(节点1->节点3) 因此只要记录上一次松驰过的边即可
模拟过程
我们依然使用minDist数组来表达起点到各个节点的最短距离,例如minDist[3] = 5 表示起点到达节点3 的最小距离为5初始化,起点为节点1,起点到起点的最短距离为0,所以minDist[1] 为 0。将节点1 加入队列 (下次松弛从节点1开始)
在这里插入图片描述

从队列里取出节点1,松弛节点1 作为出发点连接的边(节点1 -> 节点2)和边(节点1 -> 节点3)
边:节点1 -> 节点2,权值为1 ,minDist[2] > minDist[1] + 1 ,更新 minDist[2] = minDist[1] + 1 = 0 + 1 = 1 。边:节点1 -> 节点3,权值为5 ,minDist[3] > minDist[1] + 5,更新 minDist[3] = minDist[1] + 5 = 0 + 5 = 5。将节点2、节点3 加入队列
在这里插入图片描述

从队列里取出节点2,松弛节点2 作为出发点连接的边(节点2 -> 节点4)和边(节点2 -> 节点5)
边:节点2 -> 节点4,权值为1 ,minDist[4] > minDist[2] + (-3) ,更新 minDist[4] = minDist[2] + (-3) = 1 + (-3) = -2 。边:节点2 -> 节点5,权值为2 ,minDist[5] > minDist[2] + 2 ,更新 minDist[5] = minDist[2] + 2 = 1 + 2 = 3 。将节点4,节点5 加入队列
在这里插入图片描述

从队列里出去节点3,松弛节点3 作为出发点连接的边。
因为没有从节点3作为出发点的边,所以这里就从队列里取出节点3就好,不用做其他操作,如图:
在这里插入图片描述

从队列中取出节点4,松弛节点4作为出发点连接的边(节点4 -> 节点6)边:节点4 -> 节点6,权值为4 ,minDist[6] > minDist[4] + 4,更新 minDist[6] = minDist[4] + 4 = -2 + 4 = 2。将节点6加入队列
在这里插入图片描述

从队列中取出节点5,松弛节点5作为出发点连接的边(节点5 -> 节点3),边(节点5 -> 节点6)
边:节点5 -> 节点3,权值为1 ,minDist[3] > minDist[5] + 1 ,更新 minDist[3] = minDist[5] + 1 = 3 + 1 = 4。边:节点5 -> 节点6,权值为-2 ,minDist[6] > minDist[5] + (-2) ,更新 minDist[6] = minDist[5] + (-2) = 3 - 2 = 1。如图,将节点3加入队列,因为节点6已经在队列里所以不用重复添加
在这里插入图片描述

所以我们在加入队列的过程可以有一个优化,用visited数组记录已经在队列里的元素,已经在队列的元素不用重复加入,从队列中取出节点6,松弛节点6 作为出发点连接的边。节点6作为终点,没有可以出发的边。同理从队列中取出节点3,也没有可以出发的边,所以直接从队列中取出。
在这里插入图片描述

这样我们就完成了基于队列优化的bellman_ford的算法模拟过程。大家可以发现基于队列优化的算法,要比bellman_ford 算法减少很多无用的松弛情况,特别是对于边数众多的大图优化效果明显。
代码实现类似于 dijkstra 的优化版本,需要将图以邻接表的形式构建

#include <iostream>
#include <vector>
#include <list>
#include <queue>
#include <climits>using namespace std;struct Edge {int to;int val;Edge(int t, int w): to(t), val(w) {}
};int main() {int n, m, x, y, val;cin >> n >> m;vector<list<Edge>> grid(n+1);vector<bool> isInQueue(n+1);// 构造邻接表for (int i = 0; i < m; ++i) {cin >> x >> y >> val;grid[x].push_back({Edge(y, val)});}int start = 1;int end = n;vector<int> minDist(n+1, INT_MAX);minDist[start] = 0;queue<int> que;que.push(start);// 使用队列while (!que.empty()) {int cur = que.front();// 从队列里取出的时候,要取消标记,我们只保证已经在队列里的元素不用重复加入isInQueue[cur] = false;que.pop();for (Edge e : grid[cur]) {int from = cur;int to = e.to;int price = e.val;if (minDist[to] > minDist[from] + price) {minDist[to] = minDist[from] + price;// 不再添加to这个下标元素 相当于已经访问过if (isInQueue[to] == false) {que.push(to);isInQueue[to] = true;}}}}if(minDist[end] == INT_MAX) cout << "unconnected" << endl;else cout << minDist[end] << endl;return 0;
}

95. 城市间货物运输 II
涉及到负权回路的情况(存在环路,权值为负,会导致无限循环值越来越小)
非负权回路,松弛 n-1 次以上不会有变换,但是设计负权回路就会越来越小
在这里插入图片描述
那么解决本题的核心思路,就是在 kama94.城市间货物运输I 的基础上,再多松弛一次,看minDist数组是否发生变化。 如果再松弛一次结果变换则存在负权回路

// Bellman_ford 算法实现
#include <iostream>
#include <vector>
#include <climits>using namespace std;int main() {int n, m, x, y, val;cin >> n >> m;vector<vector<int>> grid;vector<int> minDist(n+1, INT_MAX);for (int i = 0; i < m; ++i) {cin >> x >> y >> val;grid.push_back({x, y, val});}int start = 1;int end = n;bool isCircle = false;minDist[start] = 0;// 多做一次负权回路for (int i = 1; i <= n; ++i) {for (vector<int>& edge : grid) {int from = edge[0];int to = edge[1];int price = edge[2];if (i < n) {if (minDist[from] != INT_MAX && minDist[to] > minDist[from] + price) {minDist[to] = minDist[from] + price;}}else {if (minDist[from] != INT_MAX && minDist[to] > minDist[from] + price) isCircle = true;}}}if (isCircle) cout << "circle" << endl;else if (minDist[end] == INT_MAX) cout << "unconnected" << endl;else cout << minDist[end] << endl;return 0;
}// Bellman_ford 队列优化版本 SPFA 实现
#include <iostream>
#include <vector>
#include <queue>
#include <list>
#include <climits>
using namespace std;struct Edge { //邻接表int to;  // 链接的节点int val; // 边的权重Edge(int t, int w): to(t), val(w) {}  // 构造函数
};int main() {int n, m, p1, p2, val;cin >> n >> m;vector<list<Edge>> grid(n + 1); // 邻接表// 将所有边保存起来for(int i = 0; i < m; i++){cin >> p1 >> p2 >> val;// p1 指向 p2,权值为 valgrid[p1].push_back(Edge(p2, val));}int start = 1;  // 起点int end = n;    // 终点vector<int> minDist(n + 1 , INT_MAX);minDist[start] = 0;queue<int> que;que.push(start); // 队列里放入起点 vector<int> count(n+1, 0); // 记录节点加入队列几次count[start]++;vector<bool> isInQueue(n+1, false);isInQueue[start] = true;bool flag = false;while (!que.empty()) {int node = que.front(); que.pop();isInQueue[node] = false;for (Edge edge : grid[node]) {int from = node;int to = edge.to;int value = edge.val;if (minDist[to] > minDist[from] + value) { // 开始松弛minDist[to] = minDist[from] + value;if (!isInQueue[to]) {que.push(to);isInQueue[to] = true;count[to]++; if (count[to] == n) {// 如果加入队列次数超过 n-1次 就说明该图与负权回路flag = true;while (!que.empty()) que.pop();break;}}}}}if (flag) cout << "circle" << endl;else if (minDist[end] == INT_MAX) {cout << "unconnected" << endl;} else {cout << minDist[end] << endl;}}

96. 城市间货物运输 III
给出尽可能路过最多城市的最短路径
本题是最多经过 k 个城市, 那么是 k + 1条边相连的节点。 这里可能有录友想不懂为什么是k + 1,来看这个图
节点1 最多已经经过2个节点 到达节点4,那么中间是有多少条边呢,是 3 条边对吧。
所以本题就是求:起点最多经过k + 1 条边到达终点的最短距离。
对所有边松弛一次,相当于计算起点到达与起点一条边相连的节点的最短距离,那么对所有边松弛 k + 1次,就是求起点到达与起点k + 1条边相连的节点的最短距离。 理解以上内容,其实本题代码就很容易了,bellman_ford 标准写法是松弛 n-1 次,本题就松弛 k + 1次就好。
但是松弛 k + 1次代码有错,具体分析如下
起点为节点1,起点到起点的距离为0,所以 minDist[1] 初始化为0 ,如图
在这里插入图片描述

其他节点对应的minDist初始化为max,因为我们要求最小距离,那么还没有计算过的节点默认是一个最大数,这样才能更新最小距离。当我们开始对所有边开始第一次松弛:边:节点1 -> 节点2,权值为-1 ,minDist[2] > minDist[1] + (-1),更新 minDist[2] = minDist[1] + (-1) = 0 - 1 = -1
在这里插入图片描述

边:节点2 -> 节点3,权值为1 ,minDist[3] > minDist[2] + 1 ,更新 minDist[3] = minDist[2] + 1 = -1 + 1 = 0 ,如图
在这里插入图片描述

边:节点3 -> 节点1,权值为-1 ,minDist[1] > minDist[3] + (-1),更新 minDist[1] = 0 + (-1) = -1 ,如图:
在这里插入图片描述

边:节点3 -> 节点4,权值为1 ,minDist[4] > minDist[3] + 1,更新 minDist[4] = 0 + 1 = 1 ,如图:
在这里插入图片描述

理论上来说,对所有边松弛一次,相当于计算 起点到达 与起点一条边相连的节点 的最短距离
而且对所有边的后面几次松弛,同样是更新了所有的节点,说明至多经过k 个节点这个限制根本没有限制住,每个节点的数值都被更新了。
这是为什么?
在上面画图距离中,对所有边进行第一次松弛,在计算边(节点2 -> 节点3) 的时候,更新了节点3。
在这里插入图片描述

理论上来说节点3 应该在对所有边第二次松弛的时候才更新。 这因为当时是基于已经计算好的 节点2(minDist[2])来做计算了。minDist[2]在计算边:(节点1 -> 节点2)的时候刚刚被赋值为 -1。
这样就造成了一个情况,即:计算minDist数组的时候,基于了本次松弛的 minDist数值,而不是上一次松弛时候minDist的数值。所以在每次计算 minDist 时候,要基于 对所有边上一次松弛的 minDist 数值才行,所以我们要记录上一次松弛的minDist。
具体代码添加了 minDist 的 copy

// 朴素 Bellman_ford方法
#include <iostream>
#include <vector>
#include <climits>using namespace std;int main() {int n, m, x, y, val, src, dst, k;cin >> n >> m;vector<vector<int>> grid;vector<int> minDist(n+1, INT_MAX);vector<int> minDistCopy(n+1);for (int i = 0; i < m; ++i) { cin >> x >> y >> val;grid.push_back({x, y, val});}cin >> src >> dst >> k;minDist[src] = 0;// 最多经过k个城市,最多走过k+1条边for (int i = 1; i <= k + 1; ++i) {// 获取上一次计算结果minDistCopy = minDist;for (vector<int>& side : grid) {int from = side[0];int to = side[1];int price = side[2];// 使用上一次copy计算if (minDistCopy[from] != INT_MAX && minDist[to] > minDistCopy[from] + price) {minDist[to] = minDistCopy[from] + price;}}}if (minDist[dst] == INT_MAX) cout << "unreachable" << endl;else cout << minDist[dst] << endl;
}// bellman_ford 队列优化 SPFA
#include <iostream>
#include <vector>
#include <queue>
#include <list>
#include <climits>using namespace std;struct Edge {int to;int val;Edge(int t, int w) : to(t), val(w) {}
};int main() {int n, m, x, y, val, src, dst, k;cin >> n >> m;vector<list<Edge>> grid(n+1);vector<int> minDist(n+1, INT_MAX);vector<int> minDistCopy(n+1);for (int i = 0; i < m; ++i) {cin >> x >> y >> val;grid[x].push_back(Edge(y, val));}cin >> src >> dst >> k;k++; // k+1minDist[src] = 0;queue<int> que;que.push(src);// k控制松弛次数int qSize = 0;while (k-- && !que.empty()) {// 开启新的队列vector<bool> isInQueue(n+1, false);minDistCopy = minDist;qSize = que.size();// 后--保证执行完全while (qSize--) {int cur = que.front();que.pop();isInQueue[cur] = false;for (Edge e : grid[cur]) {int from = cur;int to = e.to;int price = e.val;if (minDist[to] > minDistCopy[from] + price) {minDist[to] = minDistCopy[from] + price;if (!isInQueue[to]) {que.push(to);isInQueue[to] = true;}}}}}if (minDist[dst] == INT_MAX) cout << "unreachable" << endl;else cout << minDist[dst] << endl;
}

边的顺序会影响每一次扩展的结果,给出边的顺序为
我上面讲解中,给出的示例是这样的:

4 4
1 2 -1
2 3 1
3 1 -1
3 4 1
1 4 3

我将示例中边的顺序改一下,给成:

4 4
3 1 -1
3 4 1
2 3 1
1 2 -1
1 4 3

相同的图就会有不同的结果
在这里插入图片描述

在这里插入图片描述

推理一遍,初始化
在这里插入图片描述
边:节点3 -> 节点1,权值为-1 ,节点3还没有被计算过,节点1 不更新。
边:节点3 -> 节点4,权值为1 ,节点3还没有被计算过,节点4 不更新。
边:节点2 -> 节点3,权值为 1 ,节点2还没有被计算过,节点3 不更新。
边:节点1 -> 节点2,权值为 -1 ,minDist[2] > minDist[1] + (-1),更新 minDist[2] = 0 + (-1) = -1
在这里插入图片描述

可以发现 同样的图,边的顺序不一样,使用版本一的代码 每次松弛更新的节点也是不一样的。
而边的顺序是随机的,是题目给我们的,所以本题我们才需要 记录上一次松弛的minDist,来保障 每一次对所有边松弛的结果。
为什么必须使用 copy?
94.城市间货物运输I,是没有负权回路的,那么多松弛多少次,对结果都没有影响。求节点1 到 节点n 的最短路径,松弛n-1 次就够了,松弛 大于 n-1次,结果也不会变。 那么在对所有边进行第一次松弛的时候,如果基于本次计算的 minDist 来计算 minDist (相当于多做松弛了),也是对最终结果没影响。
95.城市间货物运输II 是判断是否有负权回路,一旦有负权回路,对所有边松弛 n-1 次以后,在做松弛 minDist 数值一定会变,根据这一点来判断是否有负权回路。所以,95.城市间货物运输II 只需要判断minDist数值变化了就行,而 minDist 的数值对不对,并不是我们关心的。
其关键在于本题的两个因素:
本题可以有负权回路,说明只要多做松弛,结果是会变的。
本题要求最多经过 k 个节点,对松弛次数是有限制的。
可以使用 dijkstra 吗?不可以因为 dijkstra 贪心策略导致找不到
参考:代码随想录


http://www.hkcw.cn/article/LPDpjDcTie.shtml

相关文章

CppCon 2014 学习:Making C++ Code Beautiful

你说的完全正确&#xff0c;也很好地总结了 C 这门语言在社区中的两种典型看法&#xff1a; C 的优点&#xff08;Praise&#xff09; 优点含义Powerful允许底层控制、系统编程、高性能计算、模板元编程、并发等多种用途Fast无运行时开销&#xff0c;接近汇编级别性能&#x…

手机照片太多了存哪里?

手机相册里塞满了旅行照片、生活碎片&#xff0c;每次清理都舍不得删&#xff1f;NAS——一款超实用的存储方案&#xff0c;让你的回忆安全又有序&#xff5e; 1️⃣自动备份解放双手 手机 / 电脑 / 相机照片全自动同步到 NAS&#xff0c;再也不用手动传文件 2️⃣远程访问像…

Java String的使用续 -- StringBuilder类和StringBuffer

文章目录 字符串的不可变性StringBuilder和StringBuffer函数使用 字符串的不可变性 字符串不可变是因为有private修饰&#xff0c;只能在类的内部使用不可以在类外使用&#xff0c;因此使用时是不可以修改字符串的 public class test {public static void main(String[] args…

关于xilinx pcie ip core管脚分配出现布局布线报错问题说明

一、问题说明 xilinx的pcie几个ip core选择的物理位置是固定的&#xff0c;那么相当于管脚就被指定了&#xff0c;但是这个可能和原理图的真实情况对不上 二、xilinx官方推荐 xilinx对pcie放置的位置是有推荐的&#xff0c;如果没有按照推荐的&#xff0c;是否有问题呢&#x…

全面了解DMEM培养基:功能、组成与应用

近年来&#xff0c;研究人员陆续报道了采用营养素、生长因子和激素取代血清&#xff0c;在无血清培养基中培养各种细胞系的方法。Mather和Sato&#xff08;BBRC, 1985&#xff09;报道在含有胰岛素、转铁蛋白、表皮生长因子、黄体生成素或促卵泡激素、生长调节素和生长激素的无…

晨控CK-FR08与西门子系列PLC配置MODBUS RTU通讯连接手册

晨控CK-FR08与西门子系列PLC配置MODBUS RTU通讯连接手册 产品说明&#xff1a; CK-FR08-A01是一款基于射频识别技术的高频RFID标签读卡器&#xff0c;读卡器工作频率为13.56MHZ&#xff0c;支持对I-CODE 2、I-CODE SLI等符合ISO15693国际标准协议格式标签的读取。读卡器内部集…

基于开源链动2+1模式AI智能名片S2B2C商城小程序的企业组织生态化重构研究

摘要&#xff1a;本文以互联网时代企业组织结构变革为背景&#xff0c;探讨开源链动21模式AI智能名片S2B2C商城小程序在推动企业从封闭式向开放式生态转型中的核心作用。通过分析传统企业资源获取模式与网络化组织生态的差异&#xff0c;结合开源链动21模式的裂变机制、AI智能名…

消息队列学习总结

1.保证消息不丢失的必要条件 生产者发送消息、生产者存储消息、消费者拉取消息&#xff0c;需要保证三大流程消息不丢失&#xff0c;缺一不可。生产者保证消息完整发送并存储至broker。broker保证存储的消息不丢失。消费者保证拉取的消息一定被消费&#xff0c;即使重启了&…

Unity基础学习(十二)Unity 物理系统之范围检测

目录 一、关于范围检测的主要API&#xff1a; 1. 盒状范围检测 Physics.OverlapBox 2. 球形范围检测 Physics.OverlapSphere 3. 胶囊范围检测 Physics.OverlapCapsule 4. 盒状检测 NonAlloc 版 5. 球形检测 NonAlloc 版 6. 胶囊检测 NonAlloc 版 二、关于API中的两个重…

05.29.2025 CCNA++ 学习完成

05.29.2025 CCNA 学习完成 从哪个遥远的冬天开始的。。。断断续续鸽了几个月(春节&#xff0c;马来西亚旅游), 实际大概学习了3个半月 通过这个学习对网络的底层有了一个更全面的认识&#xff0c;后面还得经常看看才能理解这些东西 课程的资料慢慢补上来 To Be continued…

Vert.x学习笔记-什么是Worker线程池

Vert.x学习笔记 一、Worker线程池的作用二、Worker线程池的特点三、Worker线程池的使用四、Worker线程池的配置与调优五、Worker线程池的工作原理1. 任务分类与线程隔离2. Worker线程池的启动与配置3. 任务提交与执行流程4. 线程安全与上下文切换5. 性能优化与监控6. 关键特性总…

Smith圆图知识学习笔记

Smith圆图知识学习笔记 理论背景 图 1 Smith图表是由菲利普史密斯(Phillip Smith)于1939年发明的,如图1所示,当时他在美国的RCA公司工作。史密斯曾说过,“在我能够使用计算尺的时候,我对以图表方式来表达数学上的关联很有兴趣”。 史密斯图表的基本在于以下的算式: 图 …

MySQL 8.0 OCP 英文题库解析(十一)

Oracle 为庆祝 MySQL 30 周年&#xff0c;截止到 2025.07.31 之前。所有人均可以免费考取原价245美元的MySQL OCP 认证。 从今天开始&#xff0c;将英文题库免费公布出来&#xff0c;并进行解析&#xff0c;帮助大家在一个月之内轻松通过OCP认证。 本期公布试题91~100 试题91…

RT-Thread Studio学习(十八)ADC+TIM+DMA

RT-Thread Studio学习&#xff08;十八&#xff09;ADCTIMDMA 一、简介二、新建RT-Thread项目并使用外部时钟三、启用ADC四、修改代码五、测试 一、简介 本文将基于STM32F407VET芯片介绍如何在RT-Thread Studio开发环境下使用ADC设备。硬件及开发环境如下&#xff1a; OS WIN…

ES分词搜索

ES的使用 前言作者使用的版本作者需求 简介ES简略介绍ik分词器简介 使用es的直接简单使用es的查询 es在java中使用备注说明 前言 作者使用的版本 es: 7.17.27spring-boot-starter-data-elasticsearch: 7.14.2 作者需求 作者接到一个业务需求&#xff0c;我们系统有份数据被…

Drawio编辑器二次开发

‌ Drawio &#xff08;现更名为 Diagrams.net &#xff09;‌是一款完全免费的在线图表绘制工具&#xff0c;由 JGraph公司 开发。它支持创建多种类型的图表&#xff0c;包括流程图、组织结构图、UML图、网络拓扑图、思维导图等&#xff0c;适用于商务演示、软件设计等多种场景…

破解高原运维难题:分布式光伏智能监控系统的应用研究

安科瑞刘鸿鹏 摘要 高原地区光照资源丰富&#xff0c;具有发展分布式光伏发电的巨大潜力。然而&#xff0c;该地区复杂的气候环境、地形地貌和运维条件对光伏电站的运行与维护带来严峻挑战。本文结合Acrel1000DP分布式光伏监控系统的技术特点和典型应用案例&#xff0c;探讨其…

Golang|分布式搜索引擎中所使用到的设计模式

迭代器模式 定义&#xff1a;在遍历接口时&#xff0c;提供统一的方法函数供调用&#xff0c;保持一致性。核心思想&#xff1a;与大众习惯保持一致&#xff0c;方便第三方实现容器类时保持一致。常见方法&#xff1a;如next()方法&#xff0c;适用于所有集合类&#xff0c;简化…

招工招聘系统开发——适配多元场景,满足企业多样化招聘需求

不同的企业有不同的招聘需求&#xff0c;不同的岗位也有不同的招聘特点。我们的招工招聘系统开发&#xff0c;充分考虑了企业的多样化需求&#xff0c;适配多元场景&#xff0c;为企业提供全方位的招聘解决方案。 对于大型企业来说&#xff0c;招聘规模大、岗位种类多、招聘流…

优化版本,增加3D 视觉 查看前面的记录

上图先 运来的超出发表上限&#xff0c;重新发。。。 #11:06:57Current_POS_is: X:77Y:471Z:0U:-2 C:\Log\V55.txt import time import tkinter as tk from tkinter import messagebox from PIL import Image, ImageTk import socket import threading from date…