最短路径之Bellman-Ford算法

最短路径之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.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 概述

基于贪心,用来计算一个点到其他任意点的最短路径的算法(也就是说,只能计算起点只有一个的情况)。不能处理存在负边权的情况。

假设起点是sdis[i] / dis[s][i]表示si的最短(路径的)距离,w[i][j]表示边<i, j>的权值,用i做中转点,来不断缩短sj的距离。此操作称作松弛(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
0x3f0x7fffffff的一半少一些,目的是为了防止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 通往奥格瑞玛的道路

三、小结

本篇结合洛谷官方书籍、灵神题单等以及我的思考~

相关推荐
格林威2 小时前
工业相机图像采集:Grab Timeout 设置建议——拒绝“假死”与“丢帧”的黄金法则
开发语言·人工智能·数码相机·计算机视觉·c#·机器视觉·工业相机
Reisentyan2 小时前
[Refactor]CPP Learn Data Day 1
c++·重构
xiaoye-duck2 小时前
C++ STL set 系列深度解析:从底层原理、核心接口到实战场景
开发语言·c++·stl
小涛不学习2 小时前
Java高频面试题(带答案版)
java·开发语言
big_rabbit05022 小时前
JVM堆内存查看命令
java·linux·算法
m0_662577972 小时前
C++中的RAII技术深入
开发语言·c++·算法
旖-旎2 小时前
二分查找(点名)(8)
c++·算法·二分查找·力扣
承渊政道2 小时前
【优选算法】(实战体验滑动窗口的奇妙之旅)
c语言·c++·笔记·学习·算法·leetcode·visual studio
lemonth2 小时前
图形推理----
人工智能·算法·机器学习