【算法笔记】最短路径算法

文章目录

一、导论

1.什么是最短路径

2.最短路径有哪些算法 分别适用于怎样的场景

约定n代表点数,m代表边数。

(1)Dijkstra算法

Dijkstra算法主要用于单源最短路径,也就是图中某一个顶点到其他顶点的最短路径的情况。且主要用于图中所有边权都是正数 的时候。

分为朴素Dijkstra算法和堆优化版的Dijkstra算法,虽然看似后者是优化版本,但并不是后者一定比前者优秀。朴素Dijkstra算法的时间复杂度是O(n2),与边数无关 ,而堆优化版的时间复杂度是O(mlogn),如果图中的边数特别密(逐渐靠近n2),那么堆优化版本的时间复杂度会接近O(n2logn),会比朴素版本更大。综上所述,当图是稠密图,边数足够多时,一般使用朴素Dijkstra算法,当图中的边很稀疏时,一般使用堆优化版Dijkstra算法

(2)Bellman-Ford算法和SPFA算法

这两个算法主要用于图中存在负权边 的情况。其中SPFA算法是Bellman-Ford算法的优化版,它的平均时间复杂度能达到O(m),最坏是O(nm)。

同样,并不是所有这种问题都是用SPFA算法,如果*对最短路径中经过的边数加以约定,如最多只能经过k条边,那么就只能使用Bellman-Ford算法

(3)Floyd算法

用于解决多源汇最短路算法,时间复杂度为O(n3)。

二、Dijkstra算法

1.朴素Dijkstra算法

朴素Dijkstra算法的核心是一个集合s,其中放的是当前已确定最短路径的点。在开始前先将第一个点放到集合中,距离为0,其余所有点的距离均设置为正无穷。然后向前走n步,每走一步就把所有不在s中的,且距离最近的点t放入s中,这意味着点t的最短距离就是当前的值,并用点t到其余点的距离更新原点到其余点的距离,相当于又往前走了一步。

代码:
AcWing 849. Dijkstra求最短路 I

cpp 复制代码
#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 510;

int n, m;
int g[N][N];//存放两点之间的距离
int dist[N];//存放源点到该点的最短距离
bool st[N];//代表到该点的最短路径是否已经确定(是否已经放入集合s)

int dijkstra()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;

    for(int i = 0; i < n - 1; i++){
        int t = -1;
        for(int j = 1; j <= n; j++){
            if(!st[j] && (t == -1 || dist[t] > dist[j]))
                t = j;
        }

        for(int j = 1; j <= n; j++){
            dist[j] = min(dist[j], dist[t] + g[t][j]);
        }

        st[t] = true;
    }

    if(dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];
}

int main()
{
    scanf("%d%d", &n, &m);

    memset(g, 0x3f, sizeof g);
    while(m--){
        int a, b, c;
        scanf("%d%d%d", &a, &b ,&c);

        g[a][b] = min(g[a][b], c);//消除重边和自环的影响
    }

    printf("%d\n", dijkstra());

    return 0;
}

2.堆优化版的Dijkstra算法

经过对朴素Dijkstra算法代码的分析,我们发现朴素算法时间复杂度高的原因是寻找未在s中的距离最小的点这一步的时间复杂度为O(n2),这一步使得整个算法的复杂度都变高了。所以我们要想让原算法变得更快,就要在这一步的基础上开始优化。

从一组数据中找出一个最小值,这让我们想起了堆这个数据结构,于是我们用堆来优化原算法,优化后能让原算法的时间复杂度降到O(n2logn)。同时由于该算法的适用场景,我们也把图改用邻接矩阵存储。
AcWing850. Dijkstra求最短路 II

cpp 复制代码
#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>

using namespace std;

typedef pair<int, int> PII;

const int N = 1e6 + 10;

int n, m;
int h[N], w[N], e[N], ne[N], idx;
int dist[N];
bool st[N];

void add(int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

int dijkstra()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    priority_queue<PII, vector<PII>, greater<PII>> heap;
    heap.push({0, 1});
    
    while(heap.size()){
        auto t = heap.top();
        heap.pop();
        
        int ver = t.second, distance = t.first;
        
        if(st[ver]) continue;
        st[ver] = true;
        
        for(int i = h[ver]; i != -1; i = ne[i]){
            int j = e[i];
            if(dist[j] > dist[ver] + w[i]){
                dist[j] = dist[ver] + w[i];
                heap.push({dist[j], j});
            }
        }
    }
    
    if(dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];
}

int main()
{
    scanf("%d%d", &n, &m);
    memset(h, -1, sizeof h);
    while(m--){
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c);
    }
    
    printf("%d\n", dijkstra());
    
    return 0;
}

三、Bellman-Ford算法和SPFA算法

1.Bellman-Ford算法

Bellman-Ford算法是普通的解决单元最短路问题的算法,并且可以解决存在负权边的图的最短路问题。

如果图中存在负权边,要先判断图中有无负权回路,负权回路是由一系列边组成的回路,且总权值为负,如果这一回路出现在路径上,理论上可无限次经过此回路使得最短路径的长度为负无穷而可能不存在最短路径的情况。

BellmanFord算法的核心是这条代码

复制代码
dist[e.b] = min(dist[e.b], last[e.a] + e.w);

这个操作叫做边的松弛 ,也就是比较在经过该点的情况下的路径距离是否比不经过的原距离更短,从而选择更好的路径。

同时,由于Bellman-Ford算法有迭代次数,所以我们可以在限定走过边的数量的情况下求最短路径,在k步之内找到最短的路径。同时,由于迭代次数的限制,这个算法还可以判断图中有无负权回路。

AcWing 853. 有边数限制的最短路

cpp 复制代码
#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 510, M = 10010;

struct Edge
{
    int a, b, w;
}edges[M];

int n, m, k;
int dist[N];
int last[N];

void bellman_ford()
{
    memset(dist, 0x3f, sizeof dist);
    
    dist[1] = 0;
    for(int i = 0; i < k; i++){
        memcpy(last, dist, sizeof dist);
        for(int j = 0; j < m; j++){
            auto e = edges[j];
            dist[e.b] = min(dist[e.b], last[e.a] + e.w);
        }
    }
}

int main()
{
    scanf("%d%d%d", &n, &m, &k);
    for(int i = 0; i < m; i++){
        int a, b, w;
        scanf("%d%d%d", &a, &b, &w);
        edges[i] = {a, b, w};
    }
    
    bellman_ford();
    
    if(dist[n] > 0x3f3f3f3f / 2) puts("impossible");
    else printf("%d\n", dist[n]);
    
    return 0;
}

2.SPFA算法

2.1 SPFA算法

Bellman-Ford算法虽然简单,但我们发现它实在太慢了,因为他对所有边都进行了松弛操作。而在所有松弛操作中,只有已经计算过的点经过的边是有效松弛。所以我们只要对上一次松弛的时候更新过的节点作为出发节点所连接的边进行松弛即可。

我们用一个队列(数据结构没有硬性要求,松弛操作与顺序无关)存储所有松弛过程中更新过的点,并用队列中的点更新他们的所有出边,这样就避免了很多无效操作。
AcWing 851. spfa求最短路

cpp 复制代码
#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>

using namespace std;

typedef pair<int, int> PII;

const int N = 1e6 + 10;

int n, m;
int h[N], w[N], e[N], ne[N], idx;
int dist[N];
bool st[N];

void add(int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

int spfa()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    
    queue<int> q;
    q.push(1);
    st[1] = true;
    
    while(q.size()){
        int t = q.front();
        q.pop();
        st[t] = false;
        
        for(int i = h[t]; i != -1; i = ne[i]){
            int j = e[i];
            if(dist[j] > dist[t] + w[i]){
                dist[j] = dist[t] + w[i];
                if(!st[j]){
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }
    
    return dist[n];
}

int main()
{
    scanf("%d%d", &n, &m);
    
    memset(h, -1, sizeof h);
    
    while(m--){
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c);
    }
    
    int t = spfa();
    if(t == 0x3f3f3f3f) puts("impossible");
    else printf("%d\n", t);
    
    return 0;
}
2.2 SPFA算法判断负权回路

由于SPFA算法可以求最短路径,所以我们可以用它来判断图中是否有负环。方法很简单,记录最短路径中点的数量,如果一条最短路径中的点数大于等于n,由抽屉原理,一定存在环,但之所以能加入最短路径,说明是可被松弛的,也就是负环。

判断方法也很简单,把所有点加入队列依次求最短路径,一边求一边记录点的个数,大于等于n时返回即可。
AcWing 852. spfa判断负环

cpp 复制代码
#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>

using namespace std;

typedef pair<int, int> PII;

const int N = 1e6 + 10;

int n, m;
int h[N], w[N], e[N], ne[N], idx;
int dist[N], cnt[N];
bool st[N];

void add(int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

int spfa()
{
    queue<int> q;
    
    for(int i = 1; i <= n; i++){
        st[i] = true;
        q.push(i);
    }

    while(q.size()){
        int t = q.front();
        q.pop();
        st[t] = false;

        for(int i = h[t]; i != -1; i = ne[i]){
            int j = e[i];
            if(dist[j] > dist[t] + w[i]){
                dist[j] = dist[t] + w[i];
                cnt[j] = cnt[t] + 1;
                
                if(cnt[j] >= n) return true;
                if(!st[j]){
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }

    return false;
}

int main()
{
    scanf("%d%d", &n, &m);

    memset(h, -1, sizeof h);

    while(m--){
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c);
    }

    if(spfa()) puts("Yes");
    else puts("No");

    return 0;
}

四、Floyd算法

Floyd算法是解决多源最短路问题的算法。Floyd算法的原理是dp。

简单来说就是在i,j两点之间插入一个中点k,看看经过点k是否能使从i到j的路径更短,写法也非常简单,就是一个三重循环中嵌套一个类似"松弛"边的代码。更详细的算法原理就需要深入dp了,以后有空再详细说明。
AcWing 854. Floyd求最短路

cpp 复制代码
#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 210, INF = 1e9;

int n, m, q;
int d[N][N];

void floyd()
{
    for(int k = 1; k <= n; k++){
        for(int i = 1; i <= n; i++){
            for(int j = 1; j <= n; j++){
                d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
            }
        }
    }
}

int main()
{
    scanf("%d%d%d", &n, &m, &q);
    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= n; j++){
            if(i == j) d[i][j] = 0;
            else d[i][j] = INF;
        }
    }
    
    while(m--){
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        d[a][b] = min(d[a][b], c);
    }
    
    floyd();
    
    while(q--){
        int a, b;
        scanf("%d%d", &a, &b);
        
        int t = d[a][b];
        if(t > INF / 2) puts("impossible");
        else printf("%d\n", t);
    }
    
    return 0;
}

总结:最短路径算法是图论中非常重要的算法,我们在面对图论问题时,最关键的一步是将问题抽象成图,把所求的问题逻辑转化成最短路径问题,然后根据题目的要求和点与边的数据规模选择合适的算法解决即可。

相关推荐
東雪木12 小时前
Java 基础语法与核心数据类型 专属复习笔记
java·开发语言·笔记·java面试
小此方12 小时前
Re:Linux系统篇(二十一)进程篇·六:穿过底层看本质,深入理解底层进程切换与 O(1) 调度算法
linux·驱动开发·算法
小O的算法实验室12 小时前
2026年SEVC,层级分解协同演化算法+带有无人机的车辆路径路径规划
算法·无人机
吃好睡好便好12 小时前
用直接输入的方式创建矩阵
开发语言·人工智能·学习·线性代数·算法·matlab·矩阵
过期动态12 小时前
【RabbitMQ高级篇】生产者可靠性、MQ可靠性、消费者可靠性以及延迟队列的实现
java·数据结构·分布式·算法·rabbitmq·ruby
问心无愧051312 小时前
ctf show web入门 254
java·开发语言·笔记
『昊纸』℃14 小时前
《C语言电子新-2026最新版》-编程语言与程序
数据结构·算法·程序设计·编程语言·软件开发
解局易否结局15 小时前
昇腾CANN上手笔记:从cann-learning-hub学会ops-transformer
笔记·深度学习·transformer
吃好睡好便好21 小时前
用while循环语句求和
开发语言·学习·算法·matlab·信息可视化