最短路径Bellman-Ford算法和SPFA算法详解

目录

Bellman-Ford算法介绍

Bellman-Ford算法证明

Bellman-Ford算法实现

SPFA算法详解

Bellman-Ford算法介绍

Dijkstra算法可以很好的解决无负权图的最短路径问题,但是如果出现了负权边,Dijkstra算法就会失效。为了更好地求解有负权边的最短路径问题,需要使用Bellman-Ford算法(简称BF算法)。和Dijkstra算法一样,Bellman-Ford算法可以解决单源最短路径问题,但也能处理有负权边的情况

现在考虑环,也就是从某个顶点出发、经过若干个不同的顶点之后可以回到该顶点的情况。而根据环中边的边权之和的正负,可以将环分为零环、正环、负环(边权之和为0、正、负)。显然,图中的零环和正环不会影响最短路径的求解,因为零环和正环的存在不能使最短路径最短;而如果图中有负环,且从源点可以到达,那么就会影响最短路径的求解;但如果图中的负环无法从源点出发到达,则最短路径的求解不会受到影响。

与Dijkstra算法相同,Bellman-Ford算法设置一个数组d,用来存放从源点到达各个顶点的最短距离。同时Bellman-Ford算法返回一个bool值:如果其中存在从源点可达的负环,那么函数将返回false;否则,函数返回true,此时数组d中存放的值就是从源点到达各顶点的最短距离。

Bellman-Ford算法的主要思路如下面的伪代码所示。需要对图中的边进行V-1轮操作,每轮都遍历图中的所有边:对每条边u->v,如果以u为中介点可以使d[v]更小,即d[u]+length[u->v]<d[v]成立时,就用d[u]+length[u->v]更新d[v]。同时也可以看到,Bellman-Ford算法的时间复杂度是O\left ( VE \right ),其中n是顶点个数,E是边数。

for(int i=0;i<n-1;i++){for(each edge u->v){if(d[u]+length[u->v]<d[v]){d[v]=d[u]+length[u->v];}}
}

此时,如果图中没有从源点可达的负环,那么数组d中的所有值都应当已经达到最优。因此,如下面的伪代码所示,只需要再对所有边进行一轮操作,判断是否有某条边u->v仍然满足d[u]+length[u->v]<d[v],如果有,则说明图中有从源点可达的负环,返回false;否则说明数组d中的所有值都已经达到最优,返回true。

for(each edge u->v){if(d[u]+length[u->v]<d[v]){return false;}return true;
}

Bellman-Ford算法证明

首先如果最短路径存在,那么最短路径上的顶点个数肯定不会超过V个。于是,如果把源点s作为一棵树的根节点,把其他结点按照最短路径的结点顺序连接,就会生成一棵最短路径树。显然,在最短路径树中,从源点S到达其余各顶点就是原图中对应的最短路径,且原图和源点一旦确定,最短路径树也就确定了。另外,由于最短路径上的顶点个数不超过V个,因此最短路径树的层数一定不超过V。

由于初始状态下d[s]为0,因此在接下来的步骤中d[s]不会被改变(也就是说,最短路径树中第一层结点d值被确定)。接着,通过Bellman-Ford算法的第一轮操作之后,最短路径中的第二层顶点的d值也会被确定下来:然后进行第二轮操作。于是第三层顶点的d值也被确定下来。这样计算直到最后一层顶点的d值确定。由于最短路径树的层数不超过V层,因此Bellman-Ford算法的松弛操作不会超过V-1轮。证毕。

Bellman-Ford算法实现

由于Bellman-Ford算法需要遍历所有边,显然使用邻接表会比较方便:如果使用邻接矩阵,则时间复杂度会上升到O\left ( V^{3} \right )。使用邻接表实现的代码如下:

struct node{int v,dis;//临界便的目标顶点,邻接边的边权 
};
vector<node> Adj[maxn];
int n;
int d[maxn];//起点到达各点的最短路径长度
bool Bellman(int s){fill(d,d+maxn,INF);d[s]=0;//求解数组d for(int i=0;i<n-1;i++){for(int u=0;u<n;u++){for(int j=0;j<Adj[u].size();j++){int v=Adj[u][j].v;int dis=Adj[u][j].dis;if(d[u]+dis<d[v]){d[v]=d[u]+dis;}}}}//判断负环for(int u=0;u<n;u++){for(int j=0;j<Adj[u].size();j++){int v=Adj[u][j].v;int dis=Adj[u][j].dis;if(d[u]+dis<d[v]){return false;}}} return true;
} 

如果在某一轮操作时,发现所有边都没有被松弛,那么说明数组d中的所有值都已经达到最优,不需要再继续,提前退出即可,这样做可以稍微加一点速度。至于最短路径的求解方法、有多重标尺时做法均与Dijkstra算法中介绍的相同,此处不再重复介绍。唯一需要注意的是统计最短路径条数的做法:由于Bellman-Ford算法期间会多次访问曾经访问过的顶点,如果单纯按照Dijkstra算法中介绍的num数组的写法,将会反复累计已经计算过的顶点。为了解决这个问题,需要设置记录前驱的数组set<int>pre[maxn],当遇到一条和已有最短路径长度相同的路径时,必须重新计算最短路径的条数。

例题

给出N个城市,M条无向边,每个城市中都有一定数目的救援小组,所有边的边权已知。现在给出起点和终点,求从起点到终点的最短路径条数及最短路径上的救援小组数目之和。如果有多条最短路径,则输出数目之和最大的。

#include<iostream>
#include<cstring>
#include<vector>
#include<set>
#include<algorithm>
using namespace std;
const int maxn=510;
const int INF=10000000000;
struct node{int v,dis;node(int _v,int _dis):v(_v),dis(_dis) {}
}; 
vector<node> Adj[maxn];
int n,m,st,ed,weight[maxn];
int d[maxn],w[maxn],num[maxn];//记录最短距离,最大点权之和,最短路径条数
set<int> pre[maxn];//前驱
void bellman(int s){fill(d,d+maxn,INF);memset(num,0,sizeof(num));memset(w,0,sizeof(w));d[s]=0;w[s]=weight[s];num[s]=1;for(int i=0;i<n-1;i++){for(int u=0;u<n;u++){for(int j=0;j<Adj[u].size();j++){int v=Adj[u][j].v;int dis=Adj[u][j].dis;if(d[u]+dis<d[v]){d[v]=d[u]+dis;w[v]=w[u]+weight[v];num[v]=num[u];pre[v].clear();pre[v].insert(u);}else if(d[u]+dis==d[v]){if(w[u]+weight[v]>w[v]){w[v]=w[u]+weight[v];}pre[v].insert(u);num[v]=0;set<int>::iterator it;for(it=pre[v].begin();it!=pre[v].end();it++){num[v]+=num[*it];}}}}}
}
int main(){cin>>n>>m>>st>>ed;for(int i=0;i<n;i++){cin>>weight[i];}int u,v,wt;for(int i=0;i<m;i++){cin>>u>>v>>wt;Adj[u].push_back(node(v,wt));Adj[v].push_back(node(u,wt));}bellman(st);cout<<num[ed]<<" "<<w[ed]<<endl;return 0;
} 

SPFA算法详解

虽然Bellman-Ford算法的思路很简洁,但是O\left ( VE \right )的时间复杂度确实很高,在很多情况下并不尽人意。仔细观察会发现,Bellman-Ford算法的每轮操作都需要操作所有边,显然这其中会有大量无意义的操作,严重影响了算法的性能。于是注意到,只有当某个顶点u的d[u]值改变时,从它出发的边的邻接点v的d[v]值才有可能被改变。由此可以进行一个优化:建立一个队列,每次将队首顶点u取出,然后对从u出发的所有边u->v进行松弛操作,也就是判断d[u]+length[u->v]<d[v]是否成立,如果成立,则用d[u]+length[u->v]覆盖d[v],于是d[v]获得更优的值,此时如果v不在队列中,就把v加入队列。这样操作直到队列为空(说明图中没有从源点可达的负环),或是某个顶点的入队次数超过V-1(说明图中存在从源点可达的负环),下面是实现的伪代码:

queue<int> Q;
源点s入队
while(队列非空){取出队首元素u;for(u的所有邻接边u->v){if(d[u]+dis<d[v]){d[v]=d[u]+dis;if(v当前不在队列){v入列;if(v入队次数大于n-1){说明有可达负环,return; } }}} 
} 

这种优化后的算法被称为SPFA算法,它的期望时间复杂度是O\left ( kE \right ),其中E是图的边数,k是一个常数,在很多情况下k不超过2,可见这个算法在大部分数据时异常高效,并且经常性地优于堆优化的Dijkstra算法。但如果图中有从源点可达的负环,传统SPFA的时间复杂度会退化为O\left ( VE \right )理解SPFA的关键是理解它是如何从Bellman-Ford算法优化得来的

下面给出邻接表表示的图的SPFA代码。如果事先知道图中不会有环,那么num数组的部分可以去掉。注意:使用SPFA可以判断是否存在从源点可达的负环,如果负环从源点不可达,则需要添加一个辅助顶点C,并添加一条从源点到达C的有向边以及V-1条从C到达除源点外各顶点的有向边才能判断负环是否存在。

vector<node> Adj[maxn];
int n,d[maxn],num[maxn];//num[maxn]记录入队次数
bool inq[maxn];
bool SPFA(int s){memset(inq,false,sizeof(inq));memset(num,0,sizeof(num));fill(d,d+maxn,INF);queue<int>Q;Q.push(s);inq[s]=true;num[s]++;d[s]=0;while(!Q.empty()){int u=Q.front();Q.pop();inq[u]=false;for(int j=0;j<Adj[u].size();j++){int v=Adj[u][j].v;int dis=Adj[u][j].dis;if(d[u]+dis<d[v]){d[v]=d[u]+dis;if(!inq[v]){Q.push(v);inq[v]=true;num[v]++;if(num[v]>=n){return false;}}}}}return true;
} 

SPFA算法十分灵活,其内部的写法可以根据具体场景的不同进行调整。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

第 5 章:面向生产的 Spring Boot

在 4.1.2 节中&#xff0c;我们介绍了 Spring Boot 的四大核心组成部分&#xff0c;第 4 章主要介绍了其中的起步依赖与自动配置&#xff0c;本章将重点介绍 Spring Boot Actuator&#xff0c;包括如何通过 Actuator 提供的各种端点&#xff08;endpoint&#xff09;了解系统的…

STM32程序启动过程

&#xff08;1&#xff09;首先对栈和堆的大小进行定义&#xff0c;并在代码区的起始处建立中断向量表&#xff0c;其第一个表项是栈顶地址&#xff08;32位&#xff09;&#xff0c;第二个表项是复位中断服务入口地址&#xff1b; &#xff08;2&#xff09;然后执行复位中断&…

人工智能AI概览

1、达特茅斯会议 2、人工智能元老 3、人工智能发展史 4、符号主义 5、连接主义 6、行为主义 7、三大学派优劣分析 8、人工智能 9、人工智能层次结构 10、机器学习、深度学习 11、人工智能现状 12、人工智能未来

HTML列表和表格标签

目录 1.列表标签 1.1无序列表 1.2有序列表 1.3定义列表 2. 表格标签、 2.1表格标签的属性 2.2合并单元格 1.列表标签 1.1无序列表 <ul>: [type 属性&#xff1a; disc( 实心圆点 )( 默认 ) 、 circle( 空心圆圈 ) 、 square( 实心方块 )] <li>: 列表中…

在VSCode中安装python

引言 Python 是一种广泛使用的高级编程语言&#xff0c;因其易学、易用、强大而受到欢迎。它由 Guido van Rossum 于 1991 年首次发布&#xff0c;并以简洁的语法和丰富的库生态系统而著称。 以下是 Python 的一些关键特点和优势&#xff1a; 关键特点 易于学习和使用&#x…

SolidWorks对设计电脑硬件配置要求是怎么样的

SolidWorks&#xff0c;作为达索系统&#xff08;Dassault Systemes&#xff09;旗下的子公司&#xff0c;一直以其出色的机械设计软件解决方案而著称。它是基于Parasolid内核开发&#xff0c;是单核三维设计软件&#xff0c;面上使用比较多的版本有SolidWorks2022、SolidWorks…

Java 数据类型 -- Java 语言的 8 种基本数据类型、字符串与数组

大家好&#xff0c;我是栗筝i&#xff0c;这篇文章是我的 “栗筝i 的 Java 技术栈” 专栏的第 004 篇文章&#xff0c;在 “栗筝i 的 Java 技术栈” 这个专栏中我会持续为大家更新 Java 技术相关全套技术栈内容。专栏的主要目标是已经有一定 Java 开发经验&#xff0c;并希望进…

6.每日LeetCode-数组类,找到所有数组中消失的数字(Go)

题目 448找到所有数组中消失的数字.go 给你一个含 n 个整数的数组 nums &#xff0c;其中 nums[i] 在区间 [1, n] 内。请你找出所有在 [1, n] 范围内但没有出现在 nums 中的数字&#xff0c;并以数组的形式返回结果。 示例 1&#xff1a; 输入&#xff1a;nums [4,3,2,7,8,2,…

MySQL 8.0 安装、配置、启动、登录、连接、卸载教程

目录 前言1. 安装 MySQL 8.01.1 下载 MySQL 8.01.2 安装 MySQL 8.0 2. 配置 MySQL 8.02.1打开环境变量2.2新建变量 MYSQL_HOME2.3编辑 Path 变量 3. 启动MySQL 8.03.1验证安装与配置是否成功3.2初始化并注册MYSQL3.3 启动MYSQL服务 4.登录MySQL4.1修改账户默认密码4.2登录MYSQL…

基于springboot实现小型诊疗预约平台系统项目【项目源码+论文说明】计算机毕业设计

基于springboot实现小型诊疗预约平台系统的设计演示 摘要 现代经济快节奏发展以及不断完善升级的信息化技术&#xff0c;让传统数据信息的管理升级为软件存储&#xff0c;归纳&#xff0c;集中处理数据信息的管理方式。本小型诊疗预约平台就是在这样的大环境下诞生&#xff0c…

西安交通大学联合中科院半导体所通过龙讯旷腾PWmat发表最新《iScience》:固定电势法揭示质子转移过程中的电子转移之谜

电化学是研究电能和化学能之间相互转换的科学。在能源日趋紧张的今天&#xff0c;研究电化学中的能量转换特性对能源科学的进步具有深远的意义。电子是电能的载体&#xff0c;因此研究电子在电化学过程中的转移是加深对电化学反应能量交换机制理解的关键。以质子电化学还原反应…

架构设计 - nginx 的核心机制与主要应用场景

一、nginx 的核心机制&#xff1a; 1. 事件驱动模型&#xff08;epoll 多路复用&#xff09; 事件循环&#xff1a; Nginx的核心组件是一个事件循环&#xff0c;它不断地监听事件&#xff08;如新连接的到来、请求数据的可读性等&#xff09;。 当有事件发生时&#xff0c;事…

深入解析TF-IDF算法:文本分析的基石与力量

在信息爆炸的时代文本数据无处不在&#xff0c;从新闻报道到社交媒体帖子&#xff0c;从学术论文到产品评论&#xff0c;大量的文本信息需要被有效地分析和利用。在这样的背景下TF-IDF&#xff08;Term Frequency-Inverse Document Frequency&#xff09;算法作为一种简单而有效…

【动态规划算法题记录】70. 爬楼梯——递归/动态规划

题目描述 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢&#xff1f; 题目分析 递归法&#xff08;超出时间限制&#xff09; 递归参数与返回值 void reversal(int i, int k) 每次我们处理第i个台阶到第k个…

redis 一些笔记1

redis 一、redis事务二、管道2.1 事务与管道的区别 三、主从复制3.13.2 权限细节3.3 基本操作命令3.4 常用3.4.1 一主几从3.4.2 薪火相传3.4.3 反客为主 3.5 步骤3.6 缺点 一、redis事务 放在一个队列里&#xff0c;依次执行&#xff0c;并不保证一致性。与mysql事务不同。 命…

一文讲清:bom管理系统是什么?在生产管理中有什么作用?

在制造业中&#xff0c;物料清单&#xff08;Bill of Materials&#xff0c;简称BOM&#xff09;扮演着至关重要的角色。物料清单&#xff08;BOM&#xff09;是制造或维修产品所需的材料、组件和零件的结构化综合列表&#xff0c;以及所需材料的数量、名称、描述和成本。简而言…

4.3 Python 元组类型常用操作及内置方法

文章目录 1. Tuple元组1.1 元组1.2 获取元素1.3 修改元素 2. 类型转换3. 索引取值与切片4. 遍历元组5. 获取长度6. 拼接与复制6.1 元组的拼接6.2 元组元素复制 7. 成员运算8. 统计元素9. 获取索引10. 练习 1. Tuple元组 1.1 元组 特征: 使用小括号括起来, 内部可以存放多个数…

【C++进阶】RBTree封装map与set

1.红黑树的迭代器 1.1 begin() begin()就是红黑树的开头&#xff0c;那么对于红黑树来说按照中序序列是该树的最左节点。 Iterator Begin(){Node* leftMin _root;while (leftMin->_left){leftMin leftMin->_left;}return Iterator(leftMin);} 1.2 end() begin()就是…

【启明智显分享】个位数价格工业HMI芯片:720P@60fps,配备2D加速

我们生活在一个“屏”的时代&#xff0c;工业自动化、智能生活的实现都离不开屏幕的帮助&#xff0c;而对于消费者而言&#xff0c;最大的痛点就是显示屏的画质&#xff0c;一个优质的人机交互界面影响着用户体验&#xff0c;流畅清晰的图像呈现与屏幕的分辨率、刷新率都息息相…

VScode ssh远程连接代码开发XHR failed

一、问题描述 在vscode下载插件Remote-SSH远程连接进行代码开发时&#xff0c;提示 XHR failed 无法建立连接。 二、解决方案 1. 离线下载vscode-server 第一步&#xff1a;vscode菜单栏----帮助----关于----提交后面的一串数字字母即为vscode的 commit_id 第二步&#xff…