PS:如果您只需要Bellman-Ford/SPFA/判负环模板,请到相应的模板部分

上一篇中简单讲解了用于多源最短路的Floyd算法。本篇要介绍的则是用与单源最短路Bellman-Ford算法和它的一些优化(包括已死的SPFA

Bellman-Ford算法

其实,和Floyd算法类似,Bellman-Ford算法同样是基于DP思想的,而且也是在不断的进行松弛操作(可以理解为「不断放宽对结果的要求」,比如在Floyd中就体现为不断第一维\(k\),具体解释在这里)

既然是单源最短路径问题,我们就不再需要在DP状态中指定起始点。于是,我们可以设计出这样的DP状态(和Floyd很类似):

\[dp[k][u]表示从s(起点)到u,最多经过k条边时的最短路径
\]

显然,初始值为:

\[对于起点s,dp[0][s]=0;对于其他任意节点u,dp[0][u]=+\infin
\]

我们可以先考虑,如何从\(dp[0][u]\)转移出\(dp[1][u]\)(就是多一条边)。

于是很容易想到这个最显而易见又最暴力的方法:枚举每一条边\((u, v)\),并更新\(dp[1][v]=min(dp[1][v], dp[0][u]+w[u][v])(其中w[u][v]表示这条边的边权)\)

推广到任意\(dp[k-1][u]\)到\(dp[k][u]\)的转移,我们仍然可以使用这样的方法。

下面是代码实现:

struct Edge {
int u, v; // 边的两个端点
int w; // 边的权值
}; int n; // 点数
int m; // 边数
Edge e[MAXM]; // 所有的边
int dp[MAXN][MAXN]; // 解释见上方 void bellman_ford(int start) {
memset(dp, 0x3f, sizeof(dp)); // 初始化为INF
dp[0][start] = 0;
for(int i = 1; i < n; i++) { // 一张图中的最长路径最多只包含n - 1边,所以更新n - 1遍就够了(因为点不能重复)
for(int j = 1; j <= n; j++) { // 先复制一遍
dp[i][j] = dp[i - 1][j];
}
for(int j = 1; j <= m; j++) { // 枚举每一条边
dp[i][e[j].v] = min(dp[i][e[j].v], dp[i - 1][e[j].u] + e[j].w);
}
}
}

显然,时间复杂度为\(O(nm)\),空间复杂度也是\(O(nm)\),代码复杂度为O(1)。

我们可以先考虑优化空间复杂度(压缩掉第一维\(k\)),于是,DP状态变为:

\[dp[u]表示从s(起点)到u的最短路径
\]

转移方程为:

\[dp[v]=min(dp[v],dp[u]+w[v])
\]

关于状态压缩后的正确性:最有可能令人不理解的部分就是:在同一轮更新中,我们可能会用已经更新完的值再去更新别的值。这就导致,同一论更新中,不同节点被更新到的DP值对应的\(k\)可能不同。(如果没看懂,就看下面这张图)

但是实际上,我们其实并不关心到底走了几步,而只关心最短路的边权和。所以,像这样的“错位更新”并不会引起错误。

于是,我们可以得到新的代码:

struct Edge {
int u, v; // 边的两个端点
int w; // 边的权值
}; int n; // 点数
int m; // 边数
Edge e[MAXM]; // 所有的边
int dp[MAXN]; // 解释见上方 void bellman_ford(int start) {
memset(dp, 0x3f, sizeof(dp)); // 初始化为INF
dp[start] = 0;
for(int i = 1; i < n; i++) { // 一张图中的最长路径最多只包含n - 1边,所以更新n - 1遍就够了(因为点不能重复)
for(int j = 1; j <= m; j++) { // 枚举每一条边
dp[e[j].v] = min(dp[e[j].v], dp[e[j].u] + e[j].w);
}
}
}

我们可以继续考虑优化时间复杂度。显然,如果在某一轮的更新后,发现并没有任何一个值被更新,那么就意味着:这张图已经不能再被更新了(已经求出\(s\)到每个点的最短路),那就可以直接break了。

所以,优化后的代码如下:

Bellman-Ford算法模板

struct Edge {
int v; // 边指向的节点
int w; // 边的权值
}; int n; // 点数
int m; // 边数
vector<Edge> g[MAXN]; // 保存从每个节点发出的边
int dp[MAXN]; // 解释见上方 void bellman_ford(int start) {
memset(dp, 0x3f, sizeof(dp)); // 初始化为INF
dp[start] = 0;
for(int i = 1; i < n; i++) { // 一张图中的最长路径最多只包含n - 1边,所以更新n - 1遍就够了(因为点不能重复)
bool updated = 0; // 记录是否有节点被更新
for(int i = 1; i <= n; i++) { // 枚举每一个节点
if(dp[i] == 0x3f3f3f3f) { // 无法到达的节点
continue;
}
for(Edge &e : g[i]) { // 枚举从这个节点发出的每一条边
if(dp[i] + e.w < dp[e.v]) {
dp[e.v] = dp[i] + e.w;
updated = 1; // 标记有值被更新
}
}
}
if(!updated) {
break; // 没有节点被更新,直接退出
}
}
}

这就是最常见的Bellman-Ford朴素算法了。

同时,也可以看到,本次优化后的代码中将「直接储存所有边」的方式改为了使用「邻接表」。这是因为邻接表在图论算法中更加常用,也使得Bellman-Ford算法可以更容易地和其他算法配合使用。

SPFA算法

SPFA算法(Shortest Path Faster Algorithm),顾名思义就是一种让Bellman-Ford跑得更快的方法。

在上一部分的最后,我们对于没有更新的情况,直接break掉,来优化时间。但是,稍加思考就会发现:有的时候,我们会为了唯一几个被更新过的节点,而再把所有的节点遍历一遍,那么这样就会产生时间的浪费。所以,SPFA本质上就是使用队列来解决这样的问题。

下面是SPFA算法的基本步骤:

  1. 我们先设置好初始值(和Bellman-Ford一样),再将起点(\(s\))加入队列中。

  2. 每次从队列中取出一个节点,尝试用它去更新与它相连的节点;如果某个节点的最短距离被更新了,那么将这个节点加入队列。

  3. 回到步骤2

于是,很容易写出对应的代码:

SPFA算法模板

struct Edge {
int v; // 边指向的节点
int w; // 边的权值
}; int n; // 点数
int m; // 边数
vector<Edge> g[MAXN]; // 保存从每个节点发出的边
int dp[MAXN]; // 定义没有变
queue<int> q; // 储存点用的队列
bool vis[MAXN]; // 记录每个节点当前是否在队列中 void spfa(int start) {
memset(dp, 0x3f, sizeof(dp)); // 初始化为INF
dp[start] = 0;
q.push(start);
vis[start] = 1; // 标记一下
while(!q.empty()) {
int x = q.front(); // 取出一个节点
q.pop();
vis[x] = 0; // 清除标记,因为下次还有可能入队
for(Edge &e : g[x]) { // 枚举从这个节点发出的每一条边
if(dp[x] + e.w < dp[e.v]) {
dp[e.v] = dp[x] + e.w;
if(!vis[e.v]) { // 如果这个节点现在不在队列中
q.push(e.v); // 那就把它加入队列
vis[e.v] = 1; // 标记一下
}
}
}
}
}

一道测试用的例题:P4779 【模板】单源最短路径(标准版)

Bellman-Ford & SPFA判断负环

负环,就是边权和为负数的环。负环是最短路算法中一个很重要的问题,因为只要进入一个负环,最短距离就会无限减小。当然,这肯定不是我们希望的,所以接下来就要介绍如何使用Bellman-Ford算法或SPFA算法来判断一张图中是否包含负环。

显然,一张有向图上的任意一条简单路径最多只包含\(n-1\)条边(否则不可能是 简单 的)。而且,当图中没有负环时,两点间的最短路径一定是简单路径。所以,如果发现从起点到某个节点\(u\)的最短路径包含多于\(n-1\)条边,那么这条路径上一定包含负环。

所以,我们只需要在算法中添加一些简单的判断就可以实现判负环了。

具体方法:

  1. 对于普通的Bellman_ford算法,我们可以在完成DP后,在进行一遍更新,如果存在任意节点与起点之间的最短路径是可以被更新的,那么可以确定图中一定存在负环

  2. 对于SPFA算法,我们可以在更新最短路径的同时,记录每条最短路径上的边数,如果发现某条最短路径的边数大于\(n-1\),那么可以确定图中一定存在负环

于是,我们可以写出分别使用这两种算法来判负环的代码:

Bellman-Ford判负环模板

struct Edge {
int v; // 边指向的节点
int w; // 边的权值
}; int n; // 点数
int m; // 边数
vector<Edge> g[MAXN]; // 保存从每个节点发出的边
int dp[MAXN]; bool bellman_ford_check_ncycle(int start) {
memset(dp, 0x3f, sizeof(dp)); // 初始化为INF
dp[start] = 0;
for(int i = 1; i < n; i++) { // 一张图中的最长路径最多只包含n - 1边,所以更新n - 1遍就够了(因为点不能重复)
bool updated = 0; // 记录是否有节点被更新
for(int i = 1; i <= n; i++) { // 枚举每一个节点
if(dp[i] == 0x3f3f3f3f) { // 无法到达的节点
continue;
}
for(Edge &e : g[i]) { // 枚举从这个节点发出的每一条边
if(dp[i] + e.w < dp[e.v]) {
dp[e.v] = dp[i] + e.w;
updated = 1; // 标记有值被更新
}
}
}
if(!updated) {
return 0; // 没有节点被更新,一定没有负环
}
}
for(int i = 1; i <= n; i++) { // 枚举每一个节点
if(dp[i] == 0x3f3f3f3f) { // 无法到达的节点
continue;
}
for(Edge &e : g[i]) { // 枚举从这个节点发出的每一条边
if(dp[i] + e.w < dp[e.v]) {
return 1; // 还能被更新说明有负环
}
}
}
return 0;
}

SPFA判负环模板

struct Edge {
int v; // 边指向的节点
int w; // 边的权值
}; int n; // 点数
int m; // 边数
vector<Edge> g[MAXN]; // 保存从每个节点发出的边
int dp[MAXN]; // dp的定义没有变
int cnt[MAXN]; // 记录从起点到节点u的最短路径中的边数
queue<int> q; // 储存点用的队列
bool vis[MAXN]; // 记录每个节点当前是否在队列中 bool spfa_check_ncycle(int start) { // SPFA判负环
memset(dp, 0x3f, sizeof(dp)); // 初始化为INF
dp[start] = 0;
q.push(start);
vis[start] = 1; // 标记一下
while(!q.empty()) {
int x = q.front(); // 取出一个节点
q.pop();
vis[x] = 0; // 清除标记,因为下次还有可能入队
for(Edge &e : g[x]) { // 枚举从这个节点发出的每一条边
if(dp[x] + e.w < dp[e.v]) {
dp[e.v] = dp[x] + e.w;
cnt[e.v] = cnt[e.v] + 1; // 多了当前这条边
if(cnt[e.v] >= n) { // 从起点到v的最短路径上有多于n - 1条边
return 1; // 一定出现了负环
}
if(!vis[e.v]) { // 如果这个节点现在不在队列中
q.push(e.v); // 那就把它加入队列
vis[e.v] = 1; // 标记一下
}
}
}
}
return 0; // 没有负环
}

一道测试用的例题:P3385 【模板】负环

Bellman-Ford算法与SPFA算法详解的更多相关文章

  1. 数据结构与算法--最短路径之Bellman算法、SPFA算法

    数据结构与算法--最短路径之Bellman算法.SPFA算法 除了Floyd算法,另外一个使用广泛且可以处理负权边的是Bellman-Ford算法. Bellman-Ford算法 假设某个图有V个顶点 ...

  2. SSD算法及Caffe代码详解(最详细版本)

    SSD(single shot multibox detector)算法及Caffe代码详解 https://blog.csdn.net/u014380165/article/details/7282 ...

  3. python 排序算法总结及实例详解

    python 排序算法总结及实例详解 这篇文章主要介绍了python排序算法总结及实例详解的相关资料,需要的朋友可以参考下 总结了一下常见集中排序的算法 排序算法总结及实例详解"> 归 ...

  4. 最短路径——Bellman-Ford算法以及SPFA算法

    说完dijkstra算法,有提到过朴素dij算法无法处理负权边的情况,这里就需要用到Bellman-Ford算法,抛弃贪心的想法,牺牲时间的基础上,换取负权有向图的处理正确. 单源最短路径 Bellm ...

  5. Bellman-ford算法、SPFA算法求解最短路模板

    Bellman-ford 算法适用于含有负权边的最短路求解,复杂度是O( VE ),其原理是依次对每条边进行松弛操作,重复这个操作E-1次后则一定得到最短路,如果还能继续松弛,则有负环.这是因为最长的 ...

  6. 关联规则算法(The Apriori algorithm)详解

    一.前言 在学习The Apriori algorithm算法时,参考了多篇博客和一篇论文,尽管这些都是很优秀的文章,但是并没有一篇文章详解了算法的整个流程,故整理多篇文章,并加入自己的一些注解,有了 ...

  7. SSD(single shot multibox detector)算法及Caffe代码详解[转]

    转自:AI之路 这篇博客主要介绍SSD算法,该算法是最近一年比较优秀的object detection算法,主要特点在于采用了特征融合. 论文:SSD single shot multibox det ...

  8. 算法笔记--sg函数详解及其模板

    算法笔记 参考资料:https://wenku.baidu.com/view/25540742a8956bec0975e3a8.html sg函数大神详解:http://blog.csdn.net/l ...

  9. Floyd算法(三)之 Java详解

    前面分别通过C和C++实现了弗洛伊德算法,本文介绍弗洛伊德算法的Java实现. 目录 1. 弗洛伊德算法介绍 2. 弗洛伊德算法图解 3. 弗洛伊德算法的代码说明 4. 弗洛伊德算法的源码 转载请注明 ...

随机推荐

  1. OpenHarmony3.1 Release版本特性解析——硬件资源池化架构介绍

    李刚 OpenHarmony 分布式硬件管理 SIG 成员 华为技术有限公司分布式硬件专家 OpenHarmony 作为面向全场景.全连接.全智能时代的分布式操作系统,通过将各类不同终端设备的能力进行 ...

  2. Spring Boot中的微信支付(小程序)

    前言 微信支付是企业级项目中经常使用到的功能,作为后端开发人员,完整地掌握该技术是十分有必要的. logo 一.申请流程和步骤 图1-1 注册微信支付账号 获取微信小程序APPID 获取微信商家的商户 ...

  3. 【Java面试】如何中断一个正在运行的线程?

    一个去京东面试的工作了5年的粉丝来找我说: Mic老师,你说并发编程很重要,果然我今天又挂在一道并发编程的面试题上了. 我问他问题是什么,他说:"如何中断一个正在运行中的线程?". ...

  4. [第18届 科大讯飞杯 J] 能到达吗

    能到达吗 题目链接:牛客5278 J 能到达吗 Description 给定一个 \(n\times m\) 的地图,地图的左上角为 \((1, 1)\) ,右下角为 \((n,m)\). 地图上有 ...

  5. MTK 虚拟 sensor bring up (pick up) sensor2.0

    pick up bring up sensor2.0 1.SCP侧的配置 (1) 放置驱动pickup.c (2) 添加底层驱动文件编译开关 (3) 加入编译文件 (4) 增加数据上报方式 (5)修改 ...

  6. Java注解和反射

    1.注解(Annotation) 1.1.什么是注解(Annotation) 注解不是程序本身,可以在程序编译.类加载和运行时被读取,并执行相应的处理.注解的格式为"@注释名(参数值)&qu ...

  7. BUUCTF-荷兰宽带数据泄露

    荷兰宽带数据泄露 下载后发现是个BIN文件,之前也是做过类似的题目 RouterPassview打开BIn文件即可,搜索username或者password. 最后flag是username

  8. 端口被占用的问题解决 Web server failed to start. Port ×× was already in use

    出现此问题是端口被占用了,只需要关闭正在使用的端口就行 解决思路: 1.在服务器中更改port端口号,改为不冲突,没有被占用的端口. 2.找出被占用的端口,结束被占用的端口 解决结束被占用的端口的方法 ...

  9. 生成RSA密钥的方法[转载]

    openssl genrsa -des3 -out privkey.pem 2048 这个命令会生成一个2048位的密钥,同时有一个des3方法加密的密码,如果你不想要每次都输入密码,可以改成(测试常 ...

  10. File类创建删除功能的方法和File类遍历(文件夹)目录功能

    File类创建删除功能的方法 -public boolean createNewFile():当且仅当具有该名称的文件尚不存在时,创建一个新的空文件 -public boolean delete(): ...