最短路径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算法十分灵活,其内部的写法可以根据具体场景的不同进行调整。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

redis 06 集群

1.节点&#xff0c;这里是把节点加到集群中的操作&#xff0c;跟主从结构不同 这里是在服务端使用命令&#xff1a; 例子&#xff1a; 2.启动节点 节点服务器 首先&#xff0c;先是服务器节点自身有一个属性来判断是不是可以使用节点功能 一般加入集群中的节点还是用r…

常见的 EVM 版本以及它们的区别

EVM&#xff08;以太坊虚拟机&#xff09;版本的演进是为了引入新的特性和改进以太坊平台的安全性、效率和功能性。每个版本通常伴随着以太坊网络的硬分叉&#xff0c;这是以太坊协议的重大升级。以下是一些常见的EVM版本及其主要区别&#xff1a; Homestead (2016年3月)&…

用h()给渲染的组件传递参数

项目的一个下载悬浮提示框是使用antv的notification组件结合自定义的进度条实现的。 效果&#xff1a; 由于进度条需要完整显示&#xff0c;所以取消了组件自带的自动关闭效果。 查看官方文档&#xff0c;可以通过notification.close(key)来关闭提示框窗口。其中key是notifica…

【odoo】odoo常用的ORM方法

概要 在Odoo中&#xff0c;ORM&#xff08;对象关系映射&#xff0c;Object-Relational Mapping&#xff09;方法是一种将Python对象映射到数据库表的方法。Odoo的ORM系统使开发者能够使用高级的Python代码而不是复杂的SQL语句来操作数据库。Odoo的ORM方法主要用于创建、读取、…

驱动开发(二):创建字符设备驱动

往期文章&#xff1a; 驱动开发&#xff08;一&#xff09;&#xff1a;驱动代码的基本框架 驱动开发&#xff08;二&#xff09;&#xff1a;创建字符设备驱动 ←本文 目录 字符驱动设备的作用 函数 字符驱动设备注册和注销 注册 注销 自动创建设备节点 创建class类…

LogicFlow 学习笔记—7. LogicFlow 基础 背景 Background

背景 Background 提供可以修改画布背景的方法&#xff0c;包括背景颜色或背景图片&#xff0c;背景层位于画布的最底层。 info 创建画布时&#xff0c;通过 background 选项来设置画布的背景层样式&#xff0c;支持透传任何样式属性到背景层。默认值为 false 表示没有背景。 …

DETR实现目标检测(二)-利用自己训练的模型进行预测

1、图片预测&#xff08;CPU&#xff09; 关于DETR模型训练自己的数据集参考上篇文章&#xff1a; DETR实现目标检测(一)-训练自己的数据集-CSDN博客 训练完成后的模型文件保存位置如下&#xff1a; 准备好要预测的图片&#xff1a; 然后直接调用模型进行预测&#xff0c;并设…

图像生成新篇章:Stable Diffusion 3 Medium开源评析

摘要 在数字艺术与人工智能的交汇点上&#xff0c;Stable Diffusion 3&#xff08;SD3&#xff09;的开源无疑是一场技术革新的盛宴。就在3月份&#xff0c;我撰写了一篇博文&#xff0c;深入探讨了SD3的技术报告内容与介绍&#xff0c;文章发表在CSDN博客上&#xff0c;https:…

docker拉取镜像失败超时的解决方法,docker配置国内镜像源

更换国内源 创建或修改 /etc/docker/daemon.json 文件 安装docker后一般只有 /etc/docker 这个目录 下面并没有 daemon.json 文件 我们直接创建 &#xff1a; vim /etc/docker/daemon.json {"registry-mirrors" : ["https://registry.docker-cn.com"…

leetcode240 搜索二维矩阵II

题目 编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性&#xff1a; 每行的元素从左到右升序排列。每列的元素从上到下升序排列。 示例 输入&#xff1a;matrix [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18…

热门开源项目OpenHarmony

目录 1.概述 1.1.开源项目的意义 1.2.开源项目对软件行业的促进作用 1.3.小结 2.OpenHarmony 2.1.技术架构 2.2.分布式软总线 2.2.1.架构 2.2.2.代码介绍 2.2.2.1.代码目录 2.2.2.2.说明 2.2.2.3.发现组网和传输 2.2.2.3.1.发现 2.2.2.3.2.组网 2.2.2.3.3.传输…

H5漂流瓶交友源码|社交漂流瓶H5源码 附安装教程

H5漂流瓶交友源码|社交漂流瓶H5源码 附安装教程 搭建教程 环境&#xff1a;Nginx 1.20.1-MySQL 5.6.50-PHP-7.3 上传源码至网站根目录&#xff0c;创建并导入数据库 数据库信息修改&#xff1a;/config/database.php 网站运行目录/public 配置文件加入&#xff08;从24行…

2. 音视频H264

视频软件基本流程 1.什么是H264 H.264是由ITU-T视频编码专家组&#xff08;VCEG&#xff09;和ISO/IEC动态图像专家组&#xff08;MPEG&#xff09;联合组成的联合视频组&#xff08;JVT&#xff0c;Joint Video Team&#xff09;提出的高度压缩数字视频编解码器标准 H265又名高…

数据预处理之基于预测的(线性,ARIMA)异常值检测#matlab

基于密度的LOF异常值检测可见上篇文章。以下介绍基于预测的异常值检测&#xff1a; 1.基于预测的异常值检测方法 基于预测的异常值检测方法&#xff0c;特别是结合线性回归和ARIMA&#xff08;自回归积分滑动平均模型&#xff09;模型&#xff0c;是数据分析中常用的技术。这…

期末复习5---PTA

以下是提交正确的代码&#xff1a; int max_len( char *s[], int n ) {int i;int max0;for(i1;i<n;i){if(strlen(s[i])>strlen(s[max]))maxi;}return strlen(s[max]); } 以下是我自己写的代码&#xff1a; 出现的问题是 &#xff1a;括号加的不对&#xff0c;需要细心…

Vue31-生命周期的简介

一、需求&#xff1a;文字的透明度递减 示例&#xff1a; 对象的简写形式 new vue({ key:value, key:value, 。。。。。。 }) 二、代码的实现 注意&#xff1a;JS不擅长小数的计算&#xff01;&#xff01;&#xff01; 此写法不好&#xff01;&#xff01;&#xff01;追求…

【抽代复习笔记】19-群(十三):奇偶置换、循环置换的几个定理及例题

定义&#xff1a; ①在Sn中&#xff0c;能够表示为奇数多个对换乘积的置换称为“奇置换”&#xff0c;能够表示为偶数多个对换乘积的置换称为“偶置换”&#xff1b; ②所有偶置换的集合记为An。 例1&#xff1a;&#xff08;1&#xff09;计算S1和S2中奇、偶置换的数目&…

QT基础-简介,安装(6.7.1编译)

目录 QT简介 一.QT编译 国内镜像网站 1. For windows a.下载:qt-everywhere-src-6.7.1.zip b.下载Cmake c.下载python d.查看readme.md e. x64 native Tools cd 到 源码目录 f.输入 g. 然后输入 ​编辑 h.最后输入 1.2. qt-creator 1.3. 配置编译 2. For Ubu…

数据结构(DS)学习笔记(4):线性表

2.1线性表的类型定义 线性表是最常用且最简单的一种数据结构&#xff0c;是一种典型的线性结构&#xff0c;一个线性表是n个数据元素的有限序列。 线性表&#xff1a;&#xff0c; ——是数据元素&#xff0c;是线性起点&#xff08;起始结点&#xff09;&#xff0c;是线性…

[数据集][目标检测]减速区域检测数据集VOC+YOLO格式1654张1类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;1654 标注数量(xml文件个数)&#xff1a;1654 标注数量(txt文件个数)&#xff1a;1654 标注…