欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 科技 > 名人名企 > 代码随想录算法训练营第六十天|Day60 图论

代码随想录算法训练营第六十天|Day60 图论

2024/11/30 3:40:59 来源:https://blog.csdn.net/a6666999d/article/details/144116435  浏览:    关键词:代码随想录算法训练营第六十天|Day60 图论

Bellman_ford 队列优化算法(又名SPFA)

https://www.programmercarl.com/kamacoder/0094.%E5%9F%8E%E5%B8%82%E9%97%B4%E8%B4%A7%E7%89%A9%E8%BF%90%E8%BE%93I-SPFA.html

本题我们来系统讲解 Bellman_ford 队列优化算法 ,也叫SPFA算法(Shortest Path Faster Algorithm)。

SPFA的称呼来自 1994年西南交通大学段凡丁的论文,其实Bellman_ford 提出后不久 (20世纪50年代末期) 就有队列优化的版本,国际上不承认这个算法是是国内提出的。 所以国际上一般称呼 该算法为 Bellman_ford 队列优化算法(Queue improved Bellman-Ford)

大家知道以上来历,知道 SPFA 和 Bellman_ford 队列优化算法 指的都是一个算法就好。

如果大家还不够了解 Bellman_ford 算法,强烈建议按照《代码随想录》的顺序学习,否则可能看不懂下面的讲解。

大家可以发现 Bellman_ford 算法每次松弛 都是对所有边进行松弛。

但真正有效的松弛,是基于已经计算过的节点在做的松弛。

给大家举一个例子:

本图中,对所有边进行松弛,真正有效的松弛,只有松弛 边(节点1->节点2) 和 边(节点1->节点3) 。

而松弛 边(节点4->节点6) ,边(节点5->节点3)等等 都是无效的操作,因为 节点4 和 节点 5 都是没有被计算过的节点。

所以 Bellman_ford 算法 每次都是对所有边进行松弛,其实是多做了一些无用功。

只需要对 上一次松弛的时候更新过的节点作为出发节点所连接的边 进行松弛就够了

基于以上思路,如何记录 上次松弛的时候更新过的节点呢?

用队列来记录。(其实用栈也行,对元素顺序没有要求)

#模拟过程

接下来来举例这个队列是如何工作的。

以示例给出的所有边为例:

5 6 -2
1 2 1
5 3 1
2 5 2
2 4 -3
4 6 4
1 3 5

1
2
3
4
5
6
7

我们依然使用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 算法 减少很多无用的松弛情况,特别是对于边数众多的大图 优化效果明显。

了解了大体流程,我们再看代码应该怎么写。

在上面模拟过程中,我们每次都要知道 一个节点作为出发点连接了哪些节点。

如果想方便知道这些数据,就需要使用邻接表来存储这个图,如果对于邻接表不了解的话,可以看 kama0047.参会dijkstra堆 中 图的存储 部分。

思路

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include <stdbool.h>#define MAXN 1000  // 假设最大节点数为1000
#define MAXM 10000 // 假设最大边数为10000typedef struct Edge {int to;   // 链接的节点int val;  // 边的权重struct Edge* next; // 指向下一个边的指针
} Edge;typedef struct {Edge* head; // 邻接表的头
} Vertex;// 创建一个新的边
Edge* createEdge(int to, int val) {Edge* edge = (Edge*)malloc(sizeof(Edge));edge->to = to;edge->val = val;edge->next = NULL;return edge;
}int main() {int n, m, p1, p2, val;scanf("%d %d", &n, &m);Vertex graph[MAXN + 1]; // 邻接表for (int i = 1; i <= n; i++) {graph[i].head = NULL; // 初始化每个节点的头节点}// 将所有边保存起来for (int i = 0; i < m; i++) {scanf("%d %d %d", &p1, &p2, &val);Edge* edge = createEdge(p2, val);edge->next = graph[p1].head; // 插入到邻接表的头部graph[p1].head = edge;}int start = 1; // 起点int end = n;   // 终点int minDist[MAXN + 1];for (int i = 1; i <= n; i++) {minDist[i] = INT_MAX; // 初始化为无穷大}minDist[start] = 0; // 起点到自身的距离为0int queue[MAXN]; // FIFO 队列int front = 0, rear = 0; // 队列的前端和后端bool isInQueue[MAXN + 1] = {false}; // 标记节点是否在队列中queue[rear++] = start; // 将起点加入队列isInQueue[start] = true;while (front < rear) {int node = queue[front++]; // 从队列中取出节点isInQueue[node] = false; // 取消标记for (Edge* edge = graph[node].head; edge != NULL; edge = edge->next) {int to = edge->to; // 获取目的节点int value = edge->val; // 获取边的权重// 松弛操作if (minDist[to] > minDist[node] + value) {minDist[to] = minDist[node] + value;if (!isInQueue[to]) { // 只有不在队列中的节点才加入queue[rear++] = to; // 加入队列isInQueue[to] = true;}}}}// 输出结果if (minDist[end] == INT_MAX) {printf("unconnected\n"); // 不能到达终点} else {printf("%d\n", minDist[end]); // 到达终点最短路径}// 释放分配的内存for (int i = 1; i <= n; i++) {Edge* edge = graph[i].head;while (edge != NULL) {Edge* temp = edge;edge = edge->next;free(temp);}}return 0;
}

学习反思

实现了使用邻接表来存储图,并利用BFS算法求解起点到终点的最短路径。

代码的主要思路如下:

  1. 定义了Edge结构体表示边,Vertex结构体表示节点。
  2. 创建了一个新的边的函数createEdge(),用于分配内存并初始化边的属性。
  3. 读取输入的节点数和边数,并创建邻接表graph。
  4. 使用for循环将所有边保存到邻接表中。
  5. 设置起点和终点,初始化最短路径数组minDist。
  6. 使用FIFO队列和BFS算法求解最短路径。
  7. 输出结果。
  8. 释放动态分配的内存。

这段代码的时间复杂度为O(n+m),其中n为节点数,m为边数。空间复杂度为O(n)。

bellman_ford之判断负权回路

https://www.programmercarl.com/kamacoder/0095.%E5%9F%8E%E5%B8%82%E9%97%B4%E8%B4%A7%E7%89%A9%E8%BF%90%E8%BE%93II.html

本题是 kama94.城市间货物运输I 延伸题目。

本题是要我们判断 负权回路,也就是图中出现环且环上的边总权值为负数。

如果在这样的图中求最短路的话, 就会在这个环里无限循环 (也是负数+负数 只会越来越小),无法求出最短路径。

所以对于 在有负权值的图中求最短路,都需要先看看这个图里有没有负权回路。

接下来我们来看 如何使用 bellman_ford 算法来判断 负权回路。

在 kama94.城市间货物运输I 中 我们讲了 bellman_ford 算法的核心就是一句话:对 所有边 进行 n-1 次松弛。 同时文中的 【拓展】部分, 我们也讲了 松弛n次以上 会怎么样?

在没有负权回路的图中,松弛 n 次以上 ,结果不会有变化。

但本题有 负权回路,如果松弛 n 次,结果就会有变化了,因为 有负权回路 就是可以无限最短路径(一直绕圈,就可以一直得到无限小的最短距离)。

那么每松弛一次,都会更新最短路径,所以结果会一直有变化。

(如果对于 bellman_ford 不了解的录友,建议详细看这里:kama94.城市间货物运输I)

以上为理论分析,接下来我们再画图举例。

我们拿题目中示例来画一个图:

图中 节点1 到 节点4 的最短路径是多少(题目中的最低运输成本) (注意边可以为负数的)

节点1 -> 节点2 -> 节点3 -> 节点4,这样的路径总成本为 -1 + 1 + 1 = 1

而图中有负权回路:

那么我们在负权回路中多绕一圈,我们的最短路径 是不是就更小了 (也就是更低的运输成本)

节点1 -> 节点2 -> 节点3 -> 节点1 -> 节点2 -> 节点3 -> 节点4,这样的路径总成本 (-1) + 1 + (-1) + (-1) + 1 + (-1) + 1 = -1

如果在负权回路多绕两圈,三圈,无穷圈,那么我们的总成本就会无限小, 如果要求最小成本的话,你会发现本题就无解了。

在 bellman_ford 算法中,松弛 n-1 次所有的边 就可以求得 起点到任何节点的最短路径,松弛 n 次以上,minDist数组(记录起到到其他节点的最短距离)中的结果也不会有改变 (如果对 bellman_ford 算法 不了解,也不知道 minDist 是什么,建议详看上篇讲解kama94.城市间货物运输I)

而本题有负权回路的情况下,一直都会有更短的最短路,所以 松弛 第n次,minDist数组 也会发生改变。

那么解决本题的 核心思路,就是在 kama94.城市间货物运输I 的基础上,再多松弛一次,看minDist数组 是否发生变化。

思路

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include <stdbool.h>#define MAXN 1000  // 假设最大节点数
#define MAXM 10000 // 假设最大边数typedef struct {int from; // 边的起点int to;   // 边的终点int weight; // 边的权重
} Edge;int main() {int n, m, p1, p2, val;scanf("%d %d", &n, &m);Edge edges[MAXM]; // 存储所有边for (int i = 0; i < m; i++) {scanf("%d %d %d", &p1, &p2, &val);edges[i].from = p1;edges[i].to = p2;edges[i].weight = val;}int start = 1; // 起点int end = n;   // 终点// 存储从起点到每个节点的最短距离int minDist[MAXN + 1];for (int i = 1; i <= n; i++) {minDist[i] = INT_MAX; // 初始化为无穷大}minDist[start] = 0; // 起点到自身的距离为0bool flag = false; // 标记是否存在负权回路for (int i = 1; i <= n; i++) { // 松弛n次for (int j = 0; j < m; j++) {int from = edges[j].from;int to = edges[j].to;int weight = edges[j].weight;if (i < n) {// 前n-1次,进行正常的松弛if (minDist[from] != INT_MAX && minDist[to] > minDist[from] + weight) {minDist[to] = minDist[from] + weight;}} else {// 第n次,检查是否存在负权回路if (minDist[from] != INT_MAX && minDist[to] > minDist[from] + weight) {flag = true;}}}}// 输出结果if (flag) {printf("circle\n"); // 存在负权回路} else if (minDist[end] == INT_MAX) {printf("unconnected\n"); // 不能到达终点} else {printf("%d\n", minDist[end]); // 到达终点的最短路径}return 0;
}

学习反思

代码是求解带权有向图的单源最短路径问题,使用了Bellman-Ford算法。

算法的思想是从起点开始,进行n-1次松弛操作,每次松弛都尝试通过当前节点更新到达其他节点的最短路径长度。最后,再进行一次松弛操作,如果还能通过该操作更新最短路径,则说明存在负权回路。

具体实现上,通过一个长度为n的数组minDist[]来记录从起点到各个节点的最短路径长度,初始化为INT_MAX,表示无穷大。

在每次松弛操作中,遍历所有边,如果从起点到当前边的起点的路径长度不为无穷大,并且通过该边可以更新到达当前边终点的最短路径长度,则更新minDist[]数组的值。

在最后一次松弛操作中,如果还可以更新最短路径,则说明存在负权回路。

最后根据minDist[end]的值来判断结果,如果为INT_MAX,则说明无法到达终点;如果为负数,则说明存在负权回路;否则,表示到达终点的最短路径长度。

这段代码的时间复杂度为O(nm),其中n为节点数,m为边数。

bellman_ford之单源有限最短路

https://www.programmercarl.com/kamacoder/0096.%E5%9F%8E%E5%B8%82%E9%97%B4%E8%B4%A7%E7%89%A9%E8%BF%90%E8%BE%93III.html

思路

本题为单源有限最短路问题,同样是 kama94.城市间货物运输I 延伸题目。

注意题目中描述是 最多经过 k 个城市的条件下,而不是一定经过k个城市,也可以经过的城市数量比k小,但要最短的路径

在 kama94.城市间货物运输I 中我们讲了:对所有边松弛一次,相当于计算 起点到达 与起点一条边相连的节点 的最短距离

节点数量为n,起点到终点,最多是 n-1 条边相连。 那么对所有边松弛 n-1 次 就一定能得到 起点到达 终点的最短距离。

(如果对以上讲解看不懂,建议详看 kama94.城市间货物运输I )

本题是最多经过 k 个城市, 那么是 k + 1条边相连的节点。 这里可能有录友想不懂为什么是k + 1,来看这个图:

图中,节点1 最多已经经过2个节点 到达节点4,那么中间是有多少条边呢,是 3 条边对吧。

所以本题就是求:起点最多经过k + 1 条边到达终点的最短距离。

对所有边松弛一次,相当于计算 起点到达 与起点一条边相连的节点 的最短距离,那么对所有边松弛 k + 1次,就是求 起点到达 与起点k + 1条边相连的节点的 最短距离。

注意: 本题是 kama94.城市间货物运输I 的拓展题,如果对 bellman_ford 没有深入了解,强烈建议先看 kama94.城市间货物运输I 再做本题。

思路

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>#define MAXN 1000  // 假设最大节点数
#define MAXM 10000 // 假设最大边数typedef struct {int from;     // 边的起点int to;       // 边的终点int weight;   // 边的权重
} Edge;int main() {int src, dst, k, p1, p2, val, m, n;// 读取节点数和边数scanf("%d %d", &n, &m);Edge edges[MAXM]; // 用于存储所有边for (int i = 0; i < m; i++) {scanf("%d %d %d", &p1, &p2, &val);edges[i].from = p1;edges[i].to = p2;edges[i].weight = val;}// 读取源节点、目标节点和最大边数kscanf("%d %d %d", &src, &dst, &k);// 存储从源节点到每个节点的最短距离int minDist[MAXN + 1];for (int i = 1; i <= n; i++) {minDist[i] = INT_MAX; // 初始化为无穷大}minDist[src] = 0; // 源节点到自身的距离为0// 松弛k + 1次for (int i = 1; i <= k + 1; i++) {int tempDist[MAXN + 1]; // 用于存储当前迭代的最短距离for (int j = 1; j <= n; j++) {tempDist[j] = minDist[j]; // 初始化临时数组}// 遍历所有边进行松弛for (int j = 0; j < m; j++) {int from = edges[j].from;int to = edges[j].to;int weight = edges[j].weight;if (tempDist[from] != INT_MAX && tempDist[to] > tempDist[from] + weight) {tempDist[to] = tempDist[from] + weight; // 更新最短路径}}// 将当前迭代的结果更新到minDistfor (int j = 1; j <= n; j++) {minDist[j] = tempDist[j];}}// 输出结果if (minDist[dst] == INT_MAX) {printf("unreachable\n"); // 不能到达目标节点} else {printf("%d\n", minDist[dst]); // 到达目标节点的最短路径}return 0;
}

学习反思

实现了求解单源最短路径问题的算法,采用了Bellman-Ford算法的思想。

首先,通过scanf函数读入节点数n和边数m。

然后,定义了一个结构体Edge用于存储边的信息,包括边的起点、终点和权重。

接下来,使用for循环读入m条边的信息,并将其存储到edges数组中。

再次使用scanf函数读入源节点src、目标节点dst和最大边数k。

然后,定义了一个数组minDist用于存储从源节点到每个节点的最短距离,初始值为无穷大。

接下来,进行k + 1次松弛操作,每次遍历所有边,如果发现存在从from节点到to节点的距离更短的路径,则更新最短距离。

最后,判断到达目标节点的最短路径是否存在,如果不存在则输出"unreachable",否则输出最短路径的值。

该算法的时间复杂度为O(k * m),其中k为最大边数,m为边数。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com