BellmanFord算法与SPFA算法

news2024/10/3 4:34:30

​​​​​​

BellmanFord算法与SPFA算法

展开

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,则也说明存在环(鸽巢原理)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/95173.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

nodejs+vue校园用车辆校车管理系统

本项目的应用场景描述如下&#xff1a;为减少学生等待校车的时间&#xff0c;合理安排校车调度&#xff0c;设计并开发一个校车预约系统&#xff0c;系统由手机端、服务器端、车载刷卡端三部分组成。学生通过手机应用&#xff08;或微信应用&#xff09;查看校车运行时段&#…

webpack系列之webpack打包图片多生成空白图片且图片不能正常加载的解决方式

文章の目录参考写在最后我用的是webpack的V5.75.0版本&#xff0c;下面是正确的配置方法 module.exports {...// 所有第三方文件模块的匹配规则module: {rules: [{test: /\.jpg|png|gif|bmp|ttf|eot|svg|woff|woff2$/,use: {loader: "url-loader",options: {limit:…

【火热报名中】2022“博客之星”年度总评选重磅启动!

技术人看过来~~2022 这一年&#xff0c;我们遇见了太多的曲折和磨砺&#xff0c;但大家依然保持初心、砥砺向前&#xff0c;用技术人的拳拳之心&#xff0c;抵挡来自时代浪潮的冲击与挑战。为嘉奖勤勉了一年的技术人&#xff0c;也为这一年的种种努力画上圆满的句号&#xff0c…

线性代数之行列式

矩阵的行列式&#xff0c;determinate&#xff08;简称det&#xff09;&#xff0c;是基于矩阵所包含的行列数据计算得到的一个标量。是为求解线性方程组而引入的。 1 行列式的定义 1.1 二阶行列式 对于二阶线性方程组 若b1b2都为0&#xff0c;则称齐次线性方程组&#xff0c;否…

Flutter - PageView(1) 基本用法

如果要实现页面切换和 Tab 布局&#xff0c;我们可以使用 PageView 组件。需要注意&#xff0c;PageView 是一个非常重要的组件&#xff0c;因为在移动端开发中很常用&#xff0c;比如大多数 App 都包含 Tab 换页效果、图片轮动以及抖音上下滑页切换视频功能等等&#xff0c;这…

【10秒在圣诞节做出温馨的圣诞树】

&#x1f935;‍♂️ 个人主页老虎也淘气 个人主页 ✍&#x1f3fb;作者简介&#xff1a;Python学习者 &#x1f40b; 希望大家多多支持我们一起进步&#xff01;&#x1f604; 如果文章对你有帮助的话&#xff0c; 欢迎评论 &#x1f4ac;点赞&#x1f44d;&#x1f3fb; 收藏…

1年就晋升了3级,全靠这份阿里大牛赠送的这份堪称神级架构师手册

又逢“金九银十”&#xff0c;年轻的毕业生们满怀希望与忐忑&#xff0c;去寻找、竞争一个工作机会。已经在职的开发同学&#xff0c;也想通过社会招聘或者内推的时机争取到更好的待遇、更大的平台。 然而&#xff0c;面试人群众多&#xff0c;技术市场却相对冷淡&#xff0c;…

spring教程

spring 1.spring ioc ​ IoC 是 Inversion of Control 的简写&#xff0c;译为“控制反转”&#xff0c;它不是一门技术&#xff0c;而是一种设计思想&#xff0c;是一个重要的面向对象编程法则&#xff0c;能够指导我们如何设计出松耦合、更优良的程序。 ​ Spring 通过 Io…

[附源码]Python计算机毕业设计Django葡萄酒销售管理系统论文

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;…

明道云与智齿科技共推个性化CRM+呼叫中心联合方案

背景介绍 近来&#xff0c;B2B企业用人及获客成本居高不下的问题愈发显现&#xff0c;企业为提高核心竞争力&#xff0c;利用信息技术协调企业与客户在销售、营销和服务上的交互&#xff0c;在优化管理方式的同时&#xff0c;向客户提供个性化交互服务&#xff0c;以达到吸引新…

[激光原理与应用-59]:激光器 - 光学 - 脉冲激光器的参数解析(能量、脉冲、周期、功率)

目录 第1章 光波的基本参数 1.1 光速 1.2 波长与频率 1.3 频率 1.4 电磁波光谱 1.5 光波的能量 第2章 脉冲激光器的参数 2.1 脉冲 2.2 脉冲宽度&#xff1a;单个脉冲作用时间。 2.3 脉冲周期/重复频率 2.4 单脉冲能量 它山之石&#xff1a; 1、激光重复频率&#…

python+Eclipse+pydev环境搭建

本文重点介绍使用Eclipsepydev插件来写Python代码&#xff0c; 以及在Mac上配置EclipsePydev 和Windows配置EclipsePydev 编辑器&#xff1a;Python 自带的 IDLE 简单快捷&#xff0c; 学习Python或者编写小型软件的时候。非常有用。 编辑器: Eclipse pydev插件 1. Eclips…

【我不熟悉的javascript】字符串正则表达式的使用match和matchAll方法

String.prototype.match() match() 方法检索返回一个字符串匹配正则表达式的结果。 参数必须是一个正则表达式如果使用 g 标志&#xff0c;则将返回与完整正则表达式匹配的所有结果&#xff0c;但不会返回捕获组。如果未使用 g 标志&#xff0c;则仅返回第一个完整匹配及其相关…

编程常见的问题(四) 连接池

编程常见的问题(四) 连接池 今天&#xff0c;我们来聊聊使用连接池需要注意的问题。 在上一讲&#xff0c;我们学习了使用线程池需要注意的问题。今天&#xff0c;我再与你说说另一种很重要的池化技术&#xff0c;即连接池。 我先和你说说连接池的结构。连接池一般对外提供获…

跨域问题及html引用JavaScript脚本问题

最近在学习 JavaScript&#xff0c;突然想到在上一家公司时跟前端同事调试 web 功能&#xff0c;然后就想着自己手动也搭一个 Http 的服务&#xff0c;这个服务是跑在一个 arm 设备上的&#xff0c;然后呢在局域网内在浏览器通过输入如 192.168.2.100:8000 这样来访问设备&…

Web(十一)JavaScript知识训练-日期对象

1、 下列关于JavaScript中Date日期对象的描述正确的是&#xff08; C&#xff09; A、 getMonth()返回Date对象的月份&#xff0c;其值介于1-12之间 B、 getDate()返回Date对象的一个月中的每一天&#xff0c;其值介于1-31之间 C、 getHours()返回Date对象的小时数&#xff0c;…

为什么要来长沙投资?我从长沙招商新政策中找到了答案!

凭借着一系列好吃的、好玩的、好看的产品&#xff0c;长沙已经顺利成为全国热门旅游目的地&#xff0c;成为全世界都小有名气的旅游城市。 作为一名在长沙创业的长沙人&#xff0c;笔者此前经常会向北上广深的朋友们推荐长沙好吃的、好玩的&#xff0c;也会向湘籍科技互联网人…

【软件测试】测试人:明明项目已经通过了各种的测试,交付还会出现问题?

目录&#xff1a;导读前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09;前言 为什么软件明明通过…

【前端开发学习】3.BootStrap

文章目录1 BootStrap2 栅格系统3 container4 面板5 案例&#xff1a;用户登录1.阴影2. 登录按钮3. 居中4. 表单项6 案例&#xff1a;后台管理7 图标8 BootStrap 依赖1 BootStrap 别人写好的 CSS 样式&#xff0c;如果想要这个 BootStrap&#xff1a; 下载 BootStrap&#xff…

Jupyter Notebook 如何安装 + 使用?【审核5次重磅发布】

人生苦短 我用python 给大家介绍一下关于Jupyter Notebook的用法 关于它的组成部分就先不在这里详细解说啦~ 毕竟我可太懂你们啦~ 文章太长就会吃灰的~ 一、什么是Jupyter Notebook&#xff1f; 1. 简介 Jupyter Notebook是基于网页的用于交互计算的应用程序。 其可被应用于…