最短路径之Bellman-Ford算法
文章目录
- 最短路径之Bellman-Ford算法
-
- 一、前言
- 二、最短路径
-
- [2.1 最短路算法](#2.1 最短路算法)
- [2.2 Floyed算法](#2.2 Floyed算法)
-
- [2.2.1 概述](#2.2.1 概述)
- [2.2.2 代码](#2.2.2 代码)
- [2.2.3 时间复杂度](#2.2.3 时间复杂度)
- [2.3 Dijkstra算法](#2.3 Dijkstra算法)
-
- [2.3.1 概述](#2.3.1 概述)
- [2.3.2 代码](#2.3.2 代码)
- [2.3.3 时间复杂度](#2.3.3 时间复杂度)
- [2.3.4 Dijkstra算法和Floyed算法的共性](#2.3.4 Dijkstra算法和Floyed算法的共性)
- [2.4 优化1-Bellman-Ford算法](#2.4 优化1-Bellman-Ford算法)
-
- [2.4.1 概述](#2.4.1 概述)
- [2.4.2 代码](#2.4.2 代码)
- [2.4.3 时间复杂度](#2.4.3 时间复杂度)
- [2.4.4 用法](#2.4.4 用法)
- [2.4.5 缺点](#2.4.5 缺点)
- [2.5 优化2-链式前向星实现的邻接表存图+堆优化版的Dijkstra算法](#2.5 优化2-链式前向星实现的邻接表存图+堆优化版的Dijkstra算法)
-
- [2.5.1 概述](#2.5.1 概述)
- [2.5.2 代码](#2.5.2 代码)
- 2.5.3时间复杂度
- [2.6 优化3-SPFA算法](#2.6 优化3-SPFA算法)
-
- [2.6.1 概述](#2.6.1 概述)
- [2.6.2 代码](#2.6.2 代码)
- [2.6.3 时间复杂度](#2.6.3 时间复杂度)
- [2.7 注意](#2.7 注意)
- [2.8 例题](#2.8 例题)
-
- [2.8.1 洛谷](#2.8.1 洛谷)
- 三、小结
一、前言
今天,是最短路径~
二、最短路径
2.1 最短路算法
| 算法 | 单源/多源 | 负边权 | 负边权回路 |
|---|---|---|---|
| Floyed算法 | 多源 | 可以 | 不可以 |
| Dijkstra算法 | 单源 | 不可以 | 不可以 |
| Bellman-Ford算法 | 单源 | 可以 | 不可以 |
| SPFA算法 | 单源 | 可以 | 不可以 |
重点关注:算法流程/思路、时间复杂度、使用情况(单源/多源,负边权/负边权回路)
负边权回路:越走越短,但是永远到不了终点
2.2 Floyed算法
2.2.1 概述
基于动态规划(后面详细讲),可以计算图中任意两点间的最短路径(多源最短路)
2.2.2 代码
cpp
// 初始化
// 点u,v有边相连
dis[u][v] = w[u][v];
// 点u,v不相连
dis[u][v] = 0x7fffffff;
for(int k = 1; k <= n; k++)
{
for(int i = 1; i <= n; i++)
{
for(int j = 1; j <= n; j++)
{
if(dis[i][j] > dis[i][k] + dis[k][j])
dis[i][j] = dis[i][k] + dis[k][j];
}
}
}
// dis[i][j]得出的就是从i到j的最短路径
2.2.3 时间复杂度
O( | v | ^ 3 )
2.3 Dijkstra算法
2.3.1 概述
基于贪心,用来计算一个点到其他任意点的最短路径的算法(也就是说,只能计算起点只有一个的情况)。不能处理存在负边权的情况。
假设起点是s,dis[i] / dis[s][i]表示s到i的最短(路径的)距离,w[i][j]表示边<i, j>的权值,用i做中转点,来不断缩短s到j的距离。此操作称作松弛(relax)
在Dijkstra算法中,对每条边执行一次松弛操作:贪心 + 松弛
2.3.2 代码
cpp
// 设起点为s,dis[v]表示从s到v的最短路径,pre[v]为v的前驱节点,用来输出路径
// 初始化
dis[v] = 0x7fffffff; // v!=s
dis[s] = 0; // 起点(自己到自己肯定是0)
pre[s] = 0;
for(int i = 1; i <= n; i++)
{
// 1.在没有被访问过的点中找一个顶点u,使得dis[u]是最小的
// 2.u标记为已确定最短路径
// 3.for 与u相连的每个未确定最短路径的顶点v(即遍历u的邻接点)
{
if(dis[u] + w[u][v] < dis[v])
{
dis[v] = dis[u] + w[u][v];
pre[v] = u;
}
}
}
2.3.3 时间复杂度
O( | v | ^ 2 )
2.3.4 Dijkstra算法和Floyed算法的共性
都找到了一个中转点(一个是贪心,离起点最近的就是中转点;另一个是枚举)
2.4 优化1-Bellman-Ford算法
2.4.1 概述
不再找中转点了,枚举边,每次将边的起点作为中转点,松弛该边,边的终点作为最短路的终点。不断枚举边,距离不变时终止
2.4.2 代码
cpp
// 设s为起点,dis[v]即为s到v的最短距离,pre[v]为v前驱(不需要就不用pre)
// w[j]是边j的长度,且j连接u、v
// 初始化
dis[s] = 0;
dis[v] = 0x7fffffff; // v!=s
pre[s] = 0;
for(int i = 1; i <= n - 1; i++) // 多少轮松弛操作
for(int j = 1; j <= 边数; j++) // 注意要枚举所有边,不能枚举点
if(dis[u] + w[j] < dis[v]) // u、v分别是这条边连接的两个点
{
dis[v] = dis[u] + w[j];
pre[v] = u;
}
++完整版++
cpp
#include<bits/stdc++.h>
#define LL long long
using namespace std;
int n, m; // n个点,m条边
int dis[105];
int s; // 起点
struct Edge
{
int a,b,w; // 起点a,终点b,权值w
}e[10005]; // 边集数组来存图
void ford()
{
int x, y, w;
bool flag = 0; // 标记是否松弛
for(int i = 1; i <= n; i++)
{
flag = 0;
for(int j = 0; j < m; j++)
{
x = e[j].a;
y = e[j].b;
w = e[j].w;
if(dis[x] + w < dis[y])
{
dis[y] = dis[x] + w;
// pre[y] = x;
flag = 1;
}
// dis[y]=min(dis[y],dis[x]+w);
}
if(flag == 0) // 说明遍历所有边之后没有松弛
{
break; // 减少几次对边的遍历
}
}
if(flag == 1) // 循环到n轮还可以进行松弛,说明存在负边权回路
{
cout << "有负权回路" << endl;
}
else
{
cout << "没有负权回路" << endl;
}
}
int main()
{
scanf("%d %d", &n, &m); // 读入n,m
for(int i = 0; i < m; i++) // 读入m条边
{
scanf("%d %d %d", &e[i].a, &e[i].b, &e[i].w);
}
scanf("%d", &s); // 读入起点s
memset(dis,0x3f,sizeof(dis)); // 设置距离为无穷大
dis[s] = 0;
ford();
for(int i = 1; i <= n; i++)
{
printf("%d ", dis[i]);
}
return 0;
}
// 测试数据
//5 5
//2 3 2
//1 2 -3
//1 5 5
//4 5 2
//3 4 3
//1
0x7fffffff是int的最大值(2^31-1)
0x是指16进制,7是0111,每一个16进制位表示4个二进制位0111 1111 1111 1111 1111 1111 1111 1111
0x3f比0x7fffffff的一半少一些,目的是为了防止dis+w越界int

2.4.3 时间复杂度
O( | v || e | )
最短路的边数最多为v - 1,最多进行 |v|-1轮松弛操作
2.4.4 用法
- 求最短路径
- 判断是否存在负边权回路
2.4.5 缺点
-
不是特别稳定
-
时间复杂度取决于建边顺序
如果可以每次松弛一定可以被松弛的边就好了,于是它的优化版本就是SPFA算法
2.5 优化2-链式前向星实现的邻接表存图+堆优化版的Dijkstra算法
2.5.1 概述
针对在没有被访问的点中找一个顶点u使得dis[u]是最小的这个步骤,利用优先队列(最小堆)实现
需要注意,点可能重复入队(具有两条最短路径)continue
2.5.2 代码
cpp
#include<bits/stdc++.h>
using namespace std;
typedef pair<int,int> PII;
int n, m, cnt;
int flag[105]; // // 标记顶点是否已确定最短距离
int dis[105];
int s; // 起点
struct Edge
{
int to, next, w;
} e[10005]; // 边集数组
int head[105];
priority_queue<PII,vector<PII>,greater<PII>> q;
void add(int x, int y, int w)
{
e[cnt].to = y;
e[cnt].w = w;
e[cnt].next = head[x];
head[x] = cnt;
cnt++;
}
void dijkstra()
{
memset(dis, 0x3f, sizeof(dis));
dis[s] = 0;// 0 s
q.push({dis[s],s}); // { }已经表示pair了
/* pair<int,int> z;
z.first = dis[s];
z.second = s;
q.push(z);
*/
while(q.size())
{
PII t = q.top();
q.pop();
int u = t.second;
int d = t.first;
if(flag[u] == 1)
{
continue;
}
flag[u] = 1;
for(int i = head[u]; i != -1; i = e[i].next)
{
// i即u的出边
int v = e[i].to;// u的邻接点
if(flag[v] == 0 && dis[v] > dis[u] + e[i].w)
{
dis[v] = dis[u] + e[i].w;
q.push({dis[v], v});
}
}
}
}
int main()
{
scanf("%d %d %d", &n, &m, &s);
int x, y, w;
memset(head, -1, sizeof(head));
for(int i = 1; i <= m; i++)
{
scanf("%d %d %d", &x, &y, &w);
add(x, y, w);
}
dijkstra();
for(int i = 1; i <= n; i++)
{
printf("%d ", dis[i]);
}
return 0;
}
2.5.3时间复杂度
O((|v|+|e|)* log|v|) (主要看Dijkstra算法)
2.6 优化3-SPFA算法
2.6.1 概述
如何明确在一轮循环中,哪条边可以松弛?
每一轮遍历中松弛的点是上一轮被松弛点的邻接点,即找上一轮被松弛点的邻接点 。则采用队列进行优化,将松弛的点入队,每次只找队列中的邻接点进行松弛。
2.6.2 代码
cpp
#include<bits/stdc++.h>
#define LL long long
using namespace std;
int n,m,cnt;
int flag[105];
int dis[105];
int use[105]; // 记录每个点被用过多少次
int s; // 起点
struct Edge
{
int to,next,w;
}e[10005];
int head[105];// 链式前向星存图
void add(int x,int y,int w)
{
e[cnt].to=y;
e[cnt].w=w;
e[cnt].next=head[x];
head[x]=cnt;
cnt++;
}
void SPFA()
{
queue<int> q; // 申请一个队列
memset(dis,0x3f,sizeof(dis));
dis[s]=0;
flag[s]=1; // 标记s点有没有被放入队列(一个点可能被放入队列多次)
// 就是标记s在上一轮中有没有被松弛的
use[s]++;
q.push(s);
while(!q.empty())
{
int u = q.front();
q.pop();
flag[u] = 0;
for(int i = head[u]; i = -1; i = e[i].next)
{
int v=e[i].to; // v就是u的邻接点
if(flag[v]==0&&dis[v]>dis[u]+e[i].w)
{
dis[v]=dis[u]+e[i].w;
q.push(v);
use[v]++;
flag[v]=1;
//if(use[v]>=n) // 入队>n次就是存在环
//{
// printf...
//}
}
}
}
}
int main()
{
scanf("%d %d %d",&n,&m,&s);
int x,y,w;
memset(head,-1,sizeof(head));
for(int i=1;i<=m;i++)
{
scanf("%d %d %d",&x,&y,&w);
add(x,y,w);
}
SPFA();
for(int i=1;i<=n;i++)
{
printf("%d ",dis[i]);
}
return 0;
}
2.6.3 时间复杂度
O(|v| * |e|),不稳定
2.7 注意
- 无负边权单源最短路:堆优化Dijkstra算法
- 多源最短路:Floyed算法
- 负边权单源最短路:SPFA算法/Ford算法
2.8 例题
2.8.1 洛谷
- P8802 [蓝桥杯 2022 国 B] 出差
- P1462 通往奥格瑞玛的道路
三、小结
本篇结合洛谷官方书籍、灵神题单等以及我的思考~