Bellman-Ford

Bellman-Ford 算法是一种用于计算带权有向图中单源最短路径(SSSP:Single-Source Shortest Path)的算法。该算法由 Richard Bellman 和 Lester Ford 分别发表于 1958 年和 1956 年,而实际上 Edward F. Moore 也在 1957 年发布了相同的算法,因此,此算法也常被称为 Bellman-Ford-Moore 算法。

Bellman-Ford 算法和 Dijkstra 算法同为解决单源最短路径的算法。对于带权有向图 G = (V, E),Dijkstra 算法要求图 G 中边的权值均为非负,而 Bellman-Ford 算法能适应一般的情况(即存在负权边的情况)。一个实现的很好的 Dijkstra 算法比 Bellman-Ford 算法的运行时间要低。

基本概念

负权边:权值为负数的边,称为负权边。

负环:环路中所有边的权值之和为负数,则称该环路为负环。

注意:带负环的图无法求最短路,因为可以沿着负环不停的循环,最短距离为负无穷大。

算法步骤

Bellman-Ford 算法采用动态规划(Dynamic Programming)进行设计,实现的时间复杂度为 O(V*E)O(V∗E),其中 VV 为顶点数量,EE 为边的数量。Dijkstra 算法采用贪心算法(Greedy Algorithm)范式进行设计,普通实现的时间复杂度为 O(V^2)O(V2),若基于堆优化的最小优先队列实现版本则时间复杂度为 O(E + VlogV)O(E+VlogV)。

Bellman-Ford 算法描述:

  1. 创建源顶点 v 到图中所有顶点的距离的集合 dis[]dis[],为图中的所有顶点指定一个距离值,初始均为 ∞∞,源顶点距离为 00;
  2. 计算最短路径,执行 V - 1V−1 次遍历(松弛边);
    • 对于图中的每条边:如果起点 uu 的距离 dd 加上边的权值 ww 小于终点 vv 的距离 dd,则更新终点 vv的距离值 dd;
  3. 检测图中是否有负权边形成了环,遍历图中的所有边,如果 dis[e.u]+e.w < dis[e.v]dis[e.u]+e.w<dis[e.v],则说明存在环;

模板题

Dijkstra求最短路 I - TopsCoding

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5+5;
int n, m, k;
struct Edge{int u,v,w;
}e[N];
int dis[N];
void bf(int s)
{memset(dis, 0x3f, sizeof dis);dis[s] = 0;for(int i = 1; i < n; i++)for(int j = 1; j <= m; j++){if(e[j].v == e[j].u) continue;   // 有自环不受影响,本条语句可以注释dis[e[j].v] = min(dis[e[j].v], dis[e[j].u] + e[j].w);}
}
int main()
{cin >> n >> m;for(int i = 1; i <= m; i++)cin >> e[i].u>>e[i].v>>e[i].w;bf(1);if(dis[n] < 0x3f3f3f3f/2) cout << dis[n];else cout << -1;return 0;
}

Copy

思考

  1. 为什么要循环 n-1n−1 次? 【回答】:因为最短路径肯定是个简单路径,不可能包含回路的。而图有 nn 个点,又不能有回路 所以最短路径最多 n-1n−1 边,又因为每次循环,至少松弛了一条边 所以最多 n-1n−1 次就行了。

  2. 为什么Dijkstra无法处理负边,而Bellman-Ford可以处理负边?【回答】:Dijkstra本质上是一种贪心策略,当有负边存在时,局部最优无法带来全局最优,贪心失效。

    Bellman-Ford本质上是一种枚举策略,在求解s →0的最短路径时,会计算所有s → b的不包含环路的路径,从中挑出权值和最小的路径。

有边数限制的最短路

问题:有边数限制的最短路 - TopsCoding

碰到限制了最短路径上边的数量时就只能用 bellman-ford 了,此时直接把上面代码中的 nn 重循环改成 kk 次循环即可

#include<bits/stdc++.h>
using namespace std;const int N=510, M=10010;struct Edge{int a;int b;int w;
}e[M]; //把每个边保存下来即可
int dis[N];
int back[N]; // 备份数组放置串联
int n,m,k; // k代表最短路径最多包涵k条边,k=n-1意味着裸最短路int bellman_ford(int s){memset(dis, 0x3f, sizeof dis);dis[s]=0;for(int i=1;i<=k;i++){ // k次循环memcpy(back,dis,sizeof dis);  // 备份上一次更新后的距离数组for(int j=1;j<=m;j++){ //遍历所有边int a=e[j].a,b=e[j].b,w=e[j].w;dis[b]=min(dis[b],back[a]+w); //使用backup原因:避免给a更新后立马更新b,这样就串联更新了}}if(dis[n]>0x3f3f3f3f/2) return -1;   // 因为存在负权边,所以无法到达的节点的 dis[] 可能比 0x3f3f3f3f 要小else return dis[n];
}int main(){cin >> n >> m >> k;for(int i=1;i<=m;i++){cin >> e[i].a >> e[i].b >> e[i].w;}int res=bellman_ford(1);if(res==-1) puts("impossible");else cout<<res;return 0;
}

Copy

值得注意的是:

1) 需要把 disdis 数组进行一个备份,这样防止每次更新的时候出现串联;
2) 由于存在负权边,所以无法到达的节点的 dis[]dis[] 可能比 0x3f3f3f3f0x3f3f3f3f要小,因此无法到达的判断条件要改成 dist[n]>0x3f3f3f3f/2;
3) 上面所谓的 nn 次遍历的实际含义是当前的最短路径最多有 n-1n−1 条边,这也就解释了为啥要 ii 遍历到 nn 的时候退出循环了,因为只有 nn 个点,最短路径无环最多就存在 n-1n−1 条边。
4) 这里无需对重边和自环做单独的处理:a. 重边:由于遍历了所有的边,总会遍历到较短的那一条; b. 自环: 有自环就有自环啊,反正又不会死循环;
5) bellman-ford 算法可以存在负权回路,因为它求得的最短路是有限制的,是限制了边数的,这样不会永久的走下去,会得到一个解
6) SPFA算法各方面优于该算法,但是在碰到限制了最短路径上边的长度时就只能用 bellman-ford了,此时直接把 nn 重循环改成 kk 次循环即可。

拓展

视频讲解:Bellman Ford 单源最短路径算法【英文中字】_bilibili

SPFA

基本原理

Bellman Ford + 队列优化 = SPFA

SPFA 算法的英文全称是 Shortest Path Faster Algorithm,从名字上我们就看得出来这个算法的最大特点就是快。它比 Bellman-ford 要快上许多倍,它的复杂度是,这里的 k是一个小于等于2的常数

SPFA 的核心原理和 Bellman-ford 算法是一样的,也是对点的松弛。只不过它优化了复杂度,优化的原理很简单, 只有被松弛过的点才有可能去松弛其他的点。优化的方法也很简单,用一个队列维护了可能存在新的松弛的点,这样我们每次从这些点出发去寻找其他可以松弛的点加入队列。

SPFA 的代码也很短,实现起来难度很低,单单从代码上来看和普通的宽搜区别并不大。

算法步骤

  1. 建立一个表格记录起始点到所有点的最短路径(该表格的初始值要赋为极大值,该点到他本身的路径赋为0)。
  2. 建立一个队列,初始时队列里只有起始点。
  3. 执行松弛操作,用队列里有的点作为起始点去刷新到所有点的最短路,如果刷新成功且被刷新点不在队列中则把该点加入到队列最后。重复执行直到队列为空。

值得注意的是

  1. Bellmanford算法里最后return-1的判断条件写的是dist[n]>0x3f3f3f3f/2;而spfa算法写的是dist[n]==0x3f3f3f3f;其原因在于Bellmanford算法会遍历所有的边,因此不管是不是和源点连通的边它都会得到更新;但是SPFA算法不一样,它相当于采用了BFS,因此遍历到的结点都是与源点连通的,因此如果你要求的n和源点不连通,它不会得到更新,还是保持的0x3f3f3f3f。
  2. 由于 SPFA 算法是由 Bellman-ford 算法优化而来,在最坏的情况下时间复杂度和它一样即时间复杂度为 O(nm)O(nm),假如题目时间复杂度允许可以直接用 SPFA 算法去解 Dijkstra 算法的题目。
  3. Bellman_ford算法可以存在负权回路,是因为其循环的次数是有限制的因此最终不会发生死循环;但是SPFA算法不可以,由于用了队列来存储,只要发生了更新就会不断的入队,因此假如有负权回路请你不要用 SPFA 否则会死循环。
  4. 求负环一般使用 SPFA 算法,方法是用一个 cntcnt 数组记录每个点到源点的边数,一个点被更新一次就 +1+1,一旦有点的边数达到了 nn 那就证明存在了负环。

模板题:spfa求最短路 - TopsCoding

参考代码:

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;typedef pair<int,int> PII;//到源点的距离,下标号
int dis[N];//各点到源点的距离
bool st[N];
int n,m;
vector<PII> g[N];int spfa(){queue<int> q;memset(dis,0x3f,sizeof dis);dis[1]=0;q.push(1);st[1]=true;while(!q.empty()){int t=q.front();q.pop();st[t]=false; // 从队列中取出来之后该节点 st 被标记为 false,代表之后该节点如果发生更新可再次入队for(int i=0;i<g[t].size();i++){int j=g[t][i].second, w = g[t][i].first;if(dis[j]>dis[t]+w){dis[j]=dis[t]+w;if(!st[j]){ // 当前已经加入队列的结点,无需再次加入队列,即便发生了更新也只用更新数值即可,重复添加降低效率st[j]=true;q.push(j);}}}}if(dis[n]==0x3f3f3f3f) return -1;else return dis[n];
}int main(){ios::sync_with_stdio(0);cin >> n >> m;while(m--){int a,b,c;cin >> a >> b >> c;g[a].push_back(make_pair(c, b));}int res=spfa();if(res==-1) puts("impossible");else cout << res;return 0;
}

Copy

备注:关于 SPFA 为什么会被卡的解释,参考这里和这里2和这里3?

判断负环

求负环的常用方法,基于 SPFA,一般都用方法 2:

方法 1:统计每个点入队的次数,如果某个点入队 nn 次,则说明存在负环

方法 2:统计当前每个点的最短路中所包含的边数,如果某点的最短路所包含的边数大于等于 nn,则也说明存在环(鸽巢原理)。

模板题:spfa判断负环 - TopsCoding

#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> PII;const int N = 100010;int n, m;
int dis[N], cnt[N]; // cnt: 统计最短路径经过的边数
bool st[N];
vector<PII> g[N];int spfa(){queue<int> q;// 题中判断的是是否可能有负环,不是一定从 1 号点出发,所以将所有可能的点都加入// 注意:第一次出队的,自己到自己也可能存在负环for(int i = 1; i <= n; i ++){st[i] = true;q.push(i);}while(q.size()){int t =q.front();q.pop();st[t] = false; // 取出的元素不在队列for(int i = 0; i < g[t].size(); i++){int j=g[t][i].second, w = g[t][i].first;if(dis[j]>dis[t]+w){dis[j] = dis[t] + w;  // 更新距离cnt[j] = cnt[t] + 1; // 等于之前t的距离加上t -> j的距离// 根据抽屉原理,说明经过某个节点两次,则说明有环if(cnt[j] >= n) return true;if(!st[j]){  // 如果不在队列中q.push(j);st[j] = true;}}}}return false;
}int main(){ios::sync_with_stdio(0);cin >> n >> m;while(m --){int a, b, c;cin >> a >> b >> c;g[a].push_back(make_pair(c, b));}if(spfa()) puts("Yes");else puts("No");return 0;
}

BellmanFord算法与SPFA算法相关推荐

  1. Bellman-Ford算法和SPFA算法

    Belloman-Ford算法 算法介绍 Dijkstra可以解决单源无负边最短路径问题.但是当遇到含有负边的单源最短路径问题就需要使用Bellman-Ford算法来解决.Bellman-Ford算法 ...

  2. 【最短路径】:Dijkstra算法、SPFA算法、Bellman-Ford算法和Floyd-Warshall算法

    求最短路径最常用的算法有: Dijkstra算法.SPFA算法.Bellman-Ford算法和Floyd-Warshall算法. Dijkstra算法.SPFA算法.Bellman-Ford算法这三个 ...

  3. 图论最短路:Bellman-Ford与其优化SPFA算法的一点理解

    文章目录 前言 一.对Bellman-Ford的深入理解 1. Bellman-Ford有什么用? 2. 什么是松弛操作? 3. Bellman-Ford的k次迭代意义? 4. 一个重要定理 5. 对 ...

  4. I won't tell you this is about graph theory----zjfc bellman-ford算法与spfa算法

    题目描述 To think of a beautiful problem description is so hard for me that let's just drop them off. :) ...

  5. 常用代码模板3——搜索与图论(Bellman-Ford算法 、spfa 算法、floyd算法、Kruskal算法、染色法、匈牙利算法 )

    目录 一.树与图的存储 二.树与图的遍历 (1) 深度优先遍历 -- 模板题 AcWing 846. 树的重心 (2) 宽度优先遍历 -- 模板题 AcWing 847. 图中点的层次 拓扑排序 -- ...

  6. 最短路径问题(Floyd算法、Dijkstra算法、Bellman-Ford算法、SPFA算法)

    导入 最短路径问题是指在一幅带权图中,找出连接两个顶点之间的所有路径中,边权和最短的那一条.如下图就是一幅带权图,边上的数字就代表该边的权值.解决最短路径问题有多种不同的算法,本文将对它们的基本思想与 ...

  7. Bellman-Ford【贝尔曼福德】算法以及SPFA算法的详细讲解,以及特殊的运用方向

    这篇文章会介绍bellman-ford算法的正确性理解和代码实现,还有SPFA算法的实现和正确性理解,以及两种代码的不同运用的方向. 先来讲一下这个算法的思路是什么: 众所周知贝尔曼福德算法的时间复杂 ...

  8. 单源最短路 Dijkstra算法 和 SPFA算法

    单源最短路 •从一个点出发,到达其他顶点的最短路径的长度. •基本操作:松弛 •d[u]+map[u, v]< d[v]这样的边(u,v)称为紧的(tense),可以对它进行松弛(relax): ...

  9. floyed算法、dijkstra算法、SPFA算法

    转自gzr的博客:https://www.cnblogs.com/TFLS-gzr/p/10381849.html,https://www.cnblogs.com/TFLS-gzr/p/1038746 ...

最新文章

  1. JavaWeb中验证码校验的功能实现
  2. Python中常用字符串 函数-转
  3. nginx虚拟目录实现两个后台使用
  4. linux下的ImageMagick安装
  5. 在mount里看到哪个设备文件的一些笔记
  6. matlab7.0编辑运行,手把手解答win10系统运行matlab7.0时提示Runtime error的操作方案
  7. linux1.0内核下载,LINUX1.0 内核是系统的心脏 - 下载 - 搜珍网
  8. Django 发送邮件
  9. windows上QT设置debugger的方法
  10. find函数常见错误_如何利用FIND找出你要的数据-EXCEL-开篇3-字符串函数系列-2
  11. wxpython播放视频_使用wxpython显示网络摄像头视频闪烁
  12. 西电2019计算机导论期中考试,西安电子科技大学203上学期期末考试计算机导论试卷.doc...
  13. 自定义ViewGroup
  14. c++面试常见问题总结
  15. 什么是HTTO协议?来看!
  16. windows 10 笔记本无法连接无线网,显示已关闭
  17. 深信服技术认证之F5隐写工具初探
  18. 五分钟搞懂什么是红黑树(全程图解)
  19. 惊艳爆了,这是我见过的最美Redis客户端
  20. ROS 机器人模型节点的运动控制原理

热门文章

  1. 微信敏感词都有什么?
  2. 30道SQL经典笔试题及其答案解析
  3. 数据驱动创新——汽车大数据生态大会在重庆召开
  4. 基于Abaqus-Simpack联合仿真车辆-浮置板轨道耦合动力学仿真
  5. Python 批量修改图片格式和尺寸
  6. html显示时钟 翻页 js,js css3翻页数字时钟代码
  7. 怎么恢复微信聊天记录
  8. 2021年整理最全Java面试题:数据结构+算法+JVM+线程+finalize+GC统统包含
  9. Linux信号量与互斥锁解决生产者与消费者问题
  10. P文件(混合星历)中各卫星更新时间和卫星总数统计