贝尔曼弗洛伊德算法
食用指南:
对该算法程序编写以及踩坑点很熟悉的同学可以直接跳转到代码模板查看完整代码
只有基础算法的题目会有关于该算法的原理,实现步骤,代码注意点,代码模板,代码误区的讲解
非基础算法的题目侧重题目分析,代码实现,以及必要的代码理解误区
题目描述:
-
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。
请你求出从 1 号点到 n 号点的最多经过 k 条边的最短距离,如果无法从 1 号点走到 n 号点,输出 impossible。
注意:图中可能 存在负权回路 。输入格式
第一行包含三个整数 n,m,k。
接下来 m 行,每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。输出格式
输出一个整数,表示从 1 号点到 n 号点的最多经过 k 条边的最短距离。
如果不存在满足条件的路径,则输出 impossible。数据范围
1≤n,k≤500,
1≤m≤10000,
任意边长的绝对值不超过 10000。输入样例:
3 3 1
1 2 1
2 3 1
1 3 3
输出样例:
3 -
题目来源:https://www.acwing.com/problem/content/855/
题目分析:
-
图论:有向图
点数n不到500,边数m不到10000 -> 稠密图 -> 邻接矩阵 -> 究竟是否要存储有待商榷
-
最多经过k条边的最短距离 -> 最短距离问题
-
从1号点到n号点 -> 单源
-
边权可能为负数 -> bellman-ford / sfpa
-
最多经过k条边 -> bellman-ford
-
下面讲解bellman-ford算法,同时为spfa做铺垫
算法原理:
模板算法:
- 传送门:朴素Dijkstra
- 传送门:堆优化Dijkstra
bellman-ford:
1. 适用情况:
-
由于bellman-ford算法侧重以边去连接点
所以对于图究竟是稠密图还是稀疏图没有要求
-
下面来看看bellman-ford是如何用边将所有点连接起来的
以及其时间复杂度和什么有关
2. 存储形式:
- 边的存储:边是bellman-ford的重中之重,其存储有两大方法
- 法一:结构体:
const int N = 100010; //边数
struct edge{
int start; //边的起点序号
int end; //边的终点序号
int len; //边长
}e[N]
- 法二:数组模拟结构体:
const int N = 100010;
int start[N];
int end[N];
int len[N];
int idx;
//对于第i条边,其起点为start[i],终点为end[i],长度为len[i]
-
各点到起点的距离:
继续使用类似Dijkstra的dis[N]; -
起点的设置:
还是类似于Dijkstra的谁是单源,就将谁的dis[]设置为0 -
图的存储:
没必要存储了,已经有了所有边的信息
遍历所有边即可找到起点发出的边,以及对应的终点
同时为了最短路径,重边在枚举边时会自动确定最短的那一条
3. 两大步骤:
-
循环轮数:
每一轮都刷新与所有已确定点相连接的未确定点的距离所以就算一轮就确定一个点到单源的最短路径,n-1轮也确定了除起点外的点到起点的距离
明确这一点后,又由于以边出发收集点,所以不必为点建立st[N],n-1轮必然确定了所有点到单源的距离
-
每轮循环遍历所有边
尝试用该边缩短边的终点到单源的距离如:单源1到2的距离为-1,到3的距离为3
但是利用边2->3的话,缩短1到3的距离为2
4. 模拟过程1:
-
初始图如下,设定1号点为单源:
-
由于有最多利用k条边的限定
所以当利用1号点更新2 5号点的dis[]后
不能马上利用2 5号点去更新 3 4号点的dis[]这样导致一轮循环,到达某点最多利用不只1条边,逐渐模糊了k条边的限制
如dis[2]&dis[5]只经过了1条边
而dis[3]&dis[4]经过了多条边 -
第1轮遍历边后:
-
第2轮遍历边后:
-
第3轮遍历边后:
-
第4轮遍历边后:
不必第4轮,已经完全利用了所有的边
最终dis[] = {0, -1, 1, -3, -3};
5. 模拟过程2:
-
可能有的同学过程图画成了这样:
-
第一轮后:
-
这是由于更新一边的起点的dis[起点]后
遍历到起点出边时,
利用更新过的dis[起点]
更新了dis[终点] -
造成了一轮循环内,若起点dis[]变化,则终点dis[]必变化,几乎所有边的终点都变化了
-
这样的现象叫做终点串联
优点是加快了最终确定dis[]的速度
缺点是丢失了最多使用k条边的限制
-
预告一下,spfa算法就是利用的终点串联
6. 分析时间复杂度:
- n-1轮大循环,每轮遍历所有边
- 共计O((n-1)·m),n为点数,m为边数
写作步骤:
- 四步走:
1. 读入边
- 结构体数组 / 静态数组模拟结构体即可
2. k轮大循环
- 每一轮只能利用一条边,最多利用k条边则最多k轮循环
3. 距离拷贝:
- 防止串联现象发生,每次遍历边时,起点都视作未被更新
- 则不存在更新一点后,将其所连边的终点也随之更新
4. 遍历边
- 遍历所有边
- 尝试利用该边将dis[终点]缩短
代码实现:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 510, M = 10010;
int n, m, k;
struct Edge{
int a, b, w; //起点 终点 边长
}e[M];
int dis[N],backup[N];//使用备份数组backup:避免给边的起点更新后马上更新边的终点
void bellman_ford(int start) {
memset(dis, 0x3f, sizeof dis);
dis[start] = 0;
for (int i = 0; i < k; i++) {//k次循环
memcpy(backup, dis, sizeof dis);
for (int j = 0; j < m; j++) { //遍历所有边
int a = e[j].a, b = e[j].b, w = e[j].w;
dis[b] = min(dis[b], backup[a] + w);
}
}
}
int main(){
cin >>n >>m >>k;
for(int i=0; i<m; i++){
int a, b, w;
cin >>a >>b >>w;
e[i] = {a,b,w};
}
int start = 1;
bellman_ford(start);
if(dis[n] > 0x3f3f3f3f/2)
cout <<"impossible";
else
cout<< dis[n];
}
代码误区:
1. 对于重边自环的处理:
-
重边由于 dis[终] = min(dis[终], dis[起]+边长);而自动选择了最短边
-
正/零自环不影响
-
负自环也不一定影响1号点到n号点的最短距离
当1号去往n号点的路上有负环,此时每多走一次负环,则最短距离减少一次
定性来说,不可能,毕竟求的就是最短距离当1号去往n号的路上无负环,则无影响
2. backup[]备份距离的作用:
-
避免终点串联现象
-
每次循环边时,都会发生dis[点]的更新
此时若接着dis[点]去更新dis[该点终点]
则到该点出边终点其实是路过了两条边但是若接着backup[点]更新dis[终点]
则该点出边终点在则这轮循环中路过了1条边 -
所以每一轮边循环更新dis[],都依据backup[]
每一轮更新dis[]完成后也是马上拷贝到backup,便于下一次更新
本篇感想:
- bellman-ford算法大体就两步:
- n-1轮循环
- 每轮循环遍历所有边,用该边尝试缩短dis[终点]
- 细节上说分四步:
- 边的读入
- n-1轮循环
- 距离的备份
- 循环边尝试缩短距离
- 看完本篇博客,恭喜已登 《筑基境-中期》
距离登仙境不远了,加油