【算法】最短路问题超详细

本文详细介绍了五种求解最短路径的算法:朴素Dijkstra算法、堆优化的Dijkstra算法、Bellman-Ford算法、SPFA算法以及Floyd算法。文章针对每种算法的思想、图示、模板题进行了讲解,并讨论了它们在处理不同情况下的适用性,如负权边、稠密图和稀疏图等。此外,还强调了在编程竞赛中理解算法原理和熟练编写代码的重要性。
摘要由CSDN通过智能技术生成

作者:指针不指南吗
专栏:算法篇

🐾算法理解没有用,会思路,会敲代码才OK🐾

0.知识结构图

每种情况下,都有一个最合适的算法

n 表示顶点的个数,m表示边的个数

在这里插入图片描述

  • 单源最短路:1号点到所有点的最短路;多元最短路:任意两个点之间的最短路

  • 稠密图和稀疏图:m是 1 0 5 10^5 105 级别的话就是稠密图,m是n级别的就是稀疏图


  • ⭐️难点

    出题的重难点在于建图 -> 把题目抽象成最短图问题,重点学习算法的实现,原理了解即可(比赛是不会考原理),做到会应用,会做题。无向图没有专门的算法,无向图即两个有向边,这里讲有向图。

  • ⭐️牢记时间复杂度

    注意时间复杂度,看一下数据范围,可以给我们很多提示

  • ⭐️学习

    算法理解没有用,会思路,会敲代码;背模板,压短写算法的时间压短,你的代码水平就会有很高的提升。

  • ⭐️调代码技巧:printf大法(出现错误答案时);删代码(出现段错误时)

1.朴素Dijkstra算法

1.1思想

集合S为已经确定最短路径的点集。

  1. 初始化距离
    一号结点的距离为零,其他结点的距离设为无穷大(看具体的题)。
  2. 循环n次,每一次将集合S之外距离最短X的点加入到S中去(这里的距离最短指的是距离1号点最近。
    点X的路径一定最短)。然后用点X更新X邻接点的距离。
  3. 两层循环,时间复杂度为 O($n^2 $)。
s[] ; //表示当前已确定的最短距离
//1.初始化距离
dis[1]=0,dis[i]=inf;  //dis表示到起点的最短距离,inf表示正无穷
//2.迭代过程,就是用个循环
for(int i=0;i<n;i++)  //每次确定一个点到起点的最短路
    t<-  //找不在s中的,距离最近的点t
    s<-t  //把t放进去,即确定好t了
    dis[x]>dis(t)+w[x]  //用 t 更新其他点的距离 
    

1.2图示

略微模糊,但是思路清晰

求 a 到 b 的最短路

从编号 1 开始 ,找起点到每个编号的最短路

红点表示已经确定最短路的顶点,

蓝色表示可以走到,但不能确定是最短路

绿色的线表示,更新当前顶点的邻接点的距离

在这里插入图片描述

1.3模板题

  • ⭐️题目

    链接: 849. Dijkstra求最短路 I - AcWing题库

    给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,所有边权均为正值。

    请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 −1。

    输入格式

    第一行包含整数 n 和 m。

    接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。

    输出格式

    输出一个整数,表示 1 号点到 n 号点的最短距离。

    如果路径不存在,则输出 −1。

    数据范围

    1≤n≤500,
    1≤m≤ 1 0 5 10^5 105 ,
    图中涉及边长均不超过10000。

    输入样例:

    3 3
    1 2 2
    2 3 1
    1 3 4
    

    输出样例:

    3
    
  • ⭐️题解

    #include<bits/stdc++.h>
    using namespace std;
    
    const int N=510;  
    int g[N][N];  //用一个邻接矩阵来存储稠密图
    int dist[N];  //表示每个点到起点的最短路
    bool st[N];  //true表示已经确定最短路,属于s集合
    int n,m;
    
    int dijkstra()  
    {
        //给距离初始化
        memset(dist,0x3f,sizeof dist); //每个顶点到起点的距离是无限大
        dist[1]=0;  //起点到起点的距离是 0
        
        //迭代即循环过程
        for(int i=1;i<=n;i++) //遍历每一个顶点每次可以确定一个点到起点的最短路
        {
            int t=-1;  //t 来存储当前访问这个点
            for(int j=1;j<=n;j++)
                if(!st[j]&&(t==-1||dist[t]>dist[j]))  //j到起点的最短路还没有确定并且t没有被更新过,或者找到比t 还短的距离
                    t=j;
            
            st[t]=true;  //已经确定当前 点 t 的最短路
            
            for(int j=1;j<=n;j++)  //利用距离最小的点,去更新其他点到其他点到起点的距离
            {
                dist[j]=min(dist[j],dist[t]+g[t][j]);  
            }
        }
        
        if(dist[n]==0x3f3f3f3f)  return -1;  //如果起点到达不了n号节点,则返回-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",dijkstra()); 
        
        return 0;
    }
    

2.堆优化版的Dijkstra算法

2.1思想

  • ⭐️思路

    堆优化版的dijkstra是对朴素版dijkstra进行了优化,在朴素版dijkstra中时间复杂度最高的寻找距离
    最短的点O( 1 0 5 10^5 105 )可以使用最小堆优化。

    1. 一号点的距离初始化为零,其他点初始化成无穷大。
    2. 将一号点放入堆中。
    3. 不断循环,直到堆空。每一次循环中执行的操作为:
      弹出堆顶(与朴素版diijkstra找到S外距离最短的点相同,并标记该点的最短路径已经确定)。
      用该点更新临界点的距离,若更新成功就加入到堆中。
  • ⭐️堆的实现

    方式手写堆优先队列(STL)
    区别可以保证n个数,支持修改堆中任意元素,使用映射不支持修改,每次修改需要新加一个数,有m个数,好写方便
  • ⭐️稠密图与稀疏图

    • 连线很多的时候,对应的就是稠密图,显然易见,稠密图的路径太多了,所以就用点来找,也就是抓重点;
    • 点很多,但是连线不是很多的时候,对应的就是稀疏图,稀疏图的路径不多,所以按照连接路径找最短路,这个过程运用优先队列,能确保每一次查找保留到更新到队列里的都是最小的,同时还解决了两个点多条路选择最短路的问题。

2.2模板题

  • ⭐️题目

    链接: 850. Dijkstra求最短路 II - AcWing题库

    给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,所有边权均为非负值。

    请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 −1。

    输入格式

    第一行包含整数 n 和 m。

    接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。

    输出格式

    输出一个整数,表示 1 号点到 n 号点的最短距离。

    如果路径不存在,则输出 −1。

    数据范围

    1≤n,m≤1.5× 1 0 5 10^5 105 ,
    图中涉及边长均不小于 0,且不超过 10000。
    数据保证:如果最短路存在,则最短路的长度不超过 1 0 9 10^9 109

    输入样例:

    3 3
    1 2 2
    2 3 1
    1 3 4
    

    输出样例:

    3
    
  • ⭐️题解

    #include<bits/stdc++.h>
    using namespace std;
    
    typedef pair<int,int> PII;
    
    const int N=1.5*1e5+10; 
    
    int n,m;
    int h[N],w[N],ne[N],e[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;  //定义根小堆,内部根据距离排序,保证堆顶是距离最小的顶点;这个顺序不能倒,pair排序时是先根据first,再根据second,
        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]; //j 当前相邻顶点的编号,i只是一个下标而不是编号
                if(dist[j]>distance+w[i])  //更新 相邻顶点到起点的最短距离
                {
                    dist[j]=distance+w[i];
                    heap.push({dist[j],j});  //距离变小则入队
                }
            }
        }
        if(dist[n]==0x3f3f3f3f) return -1;  //如果n到起点的距离是正无穷,说明n与起点不通
        return dist[n]; //否在,返回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",dijkstra());
        
        return 0;
    }
    

3.Bellman-Ford算法

3.1思想

  • ⭐️为什么 Dijkstra 算法不能解决负权边?

    • dijkstra要求每个点被确定后st[j] = true,dist[j]就是最短距离了,之后就不能再被更新了(一锤子买卖),而如果有负权边的话,那已经确定的点的dist[j]不一定是最短了。

    • dijstra算法基于贪心思想,当有负权边时,局部最优不一定是全局最优

  • ⭐️什么是Bellman-Ford算法?

    • Bellman - ford 算法是求含负权图的单源最短路径的一种算法,效率较低,代码难度较小。其原理为连续进行松弛,在每次松弛时把每条边都更新一下,若在n-1 次松弛后还能更新,则说明图中有负环,因此无法得出结果,否则就完成。
    • 通俗的来讲就是:假设 1 号点到 n 号点是可达的,每一个点同时向指向的方向出发,更新相邻的点的最短距离,通过循环 n-1 次操作,若图中不存在负环,则 1 号点一定会到达 n 号点,若图中存在负环,则在 n-1 次松弛后一定还会更新。
  • ⭐️步骤

    for n次 //表示更新n条边,还要记得备注
    	for 所有边 a,b,w (松弛操作)
    		dist[b] = min(dist[b],back[a] + w)
    
    //注意:back[] 数组是上一次迭代后 dist[] 数组的备份,由于是每个点同时向外出发,因此需要对 dist[] 数组进行备份,若不进行备份会因此发生串联效应,影响到下一个点
    
  • ⭐️是否能到达n号点的判断中需要进行if(dist[n] > INF/2)判断

    • 在下面代码中,是否能到达n号点的判断中需要进行if(dist[n] > INF/2)判断,而并非是if(dist[n] == INF)判断,原因是INF是一个确定的值,并非真正的无穷大,会随着其他数值而受到影响,dist[n]大于某个与INF相同数量级的数即可

    • 比如5号节点距离起点的距离是无穷大,利用5号节点更新n号节点距离起点的距离,将得到 1 0 5 10^5 105 −2,它 虽然小于 1 0 9 10^9 109 , 但并不存在最短路,(在边数限制在k条的条件下)。

  • ⭐️只要题中没有负环就可以用这个算法

    有负权回路,是没有最短路的,负权回路的最小路就是负无穷

  • ⭐️但是如果有边数限制的话,求最短路有负权回路也就无所谓了,也只能使用Bellman-Ford算法

    举个例子:比如乘客从某个城市到另一个城市,之间可以进行周转,但是没周转一次,乘客的心情就会变坏一次,所以限制周转的次数为k次,这样就限制了边的次数

3.2模板题

  • ⭐️题目

    链接: 853. 有边数限制的最短路 - AcWing题库

    给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数

    请你求出从 11 号点到 n 号点的最多经过 k 条边的最短距离,如果无法从 1 号点走到 n 号点,输出 impossible

    注意:图中可能 存在负权回路

    输入格式

    第一行包含三个整数 n,m,k。

    接下来 m 行,每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。

    点的编号为 1∼n。

    输出格式

    输出一个整数,表示从 1 号点到 n 号点的最多经过 k 条边的最短距离。

    如果不存在满足条件的路径,则输出 impossible

    数据范围

    1≤n,k≤500,
    1≤m≤10000,
    1≤x,y≤n,
    任意边长的绝对值不超过 10000。

    输入样例:

    3 3 1
    1 2 1
    2 3 1
    1 3 3
    

    输出样例:

    3
    
  • ⭐️题解

    #include<bits/stdc++.h>
    using namespace std;
    
    const int N=510,M=10010;
    
    int n,m,k;
    int dist[N],backup[N];  //dist表示该点到起点的最短路,backup用来备份,具体看函数内部实现
    
    struct Edge{
        int a,b,w;
    }edges[M];  //a到b的距离为 w
    
    int bellman_ford()
    {
        memset(dist,0x3f,sizeof dist);  //初始化 每个顶点到起点的最短路
        dist[1]=0;
        
        for(int i=0;i<k;i++)  //遍历 k 条边
        {
            memcpy(backup,dist,sizeof dist);  //备份
            for(int j=0;j<m;j++)  //遍历所有遍,找最短路
            {
                int a=edges[j].a,b=edges[j].b,w=edges[j].w;   //松弛,更新边最短路
                dist[b]=min(dist[b],backup[a]+w);
            }
        }
        
        if(dist[n]>0x3f3f3f3f/2)  return 0x3f3f3f3f/2+1;  //n到起点的距离是无穷,表示n到起点不通
        return dist[n];  //否则返回 n到起点的最短路
    }
    
    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};  //加边
        }
        
        int t=bellman_ford();
        
        if(t>0x3f3f3f3f/2) puts("impossible");
        else printf("%d\n",t);
        
        return 0;
    }
    

4.SPFA算法

4.1思想

  • ⭐️分析
    SPFA算法仅仅只是对该Bellman-ford算法的一个优化。

    我觉得吧,区别就是Bellman-Ford没有用邻接表,而SPFA用了邻接表

    Bellman_ford算法会遍历所有的边,但是有很多的边遍历了其实没有什么意义,我们只用遍历那些到源点距离变小的点所连接的边即可,只有当一个点的前驱结点更新了,该节点才会得到更新;因此考虑到这一点,我们将创建一个队列每一次加入距离被更新的结点。

  • ⭐️注意

    1. st数组的作用:判断当前的点是否已经加入到队列当中了;已经加入队列的结点就不需要反复的把该点加入到队列中了,就算此次还是会更新到源点的距离,那只用更新一下数值而不用加入到队列当中。
      即便不使用st数组最终也没有什么关系,但是使用的好处在于可以提升效率。

    2. SPFA算法看上去和 Dijstra 算法长得有一些像但是其中的意义还是相差甚远的:

      1. Dijkstra算法中的st数组保存的是当前确定了到源点距离最小的点,且一旦确定了最小那么就不可逆了(不可标记为true后改变为false);

        SPFA算法中的st数组仅仅只是表示的当前发生过更新的点,且spfa中的st数组可逆(可以在标记为true之后又标记为false)。顺带一提的是BFS中的st数组记录的是当前已经被遍历过的点。

      2. Dijkstra算法里使用的是优先队列保存的是当前未确定最小距离的点,目的是快速的取出当前到源点距离最小的点;

        SPFA算法中使用的是队列(你也可以使用别的数据结构),目的只是记录一下当前发生过更新的点。

    3. Bellman-ford算法里最后return-1的判断条件写的是dist[n]>0x3f3f3f3f/2;而spfa算法写的是dist[n]==0x3f3f3f3f;

      其原因在于Bellman_ford算法会遍历所有的边,因此不管是不是和源点连通的边它都会得到更新;但是SPFA算法不一样,它相当于采用了BFS,因此遍历到的结点都是与源点连通的,因此如果你要求的n和源点不连通,它不会得到更新,还是保持的0x3f3f3f3f。

    4. Bellman_ford算法可以存在负权回路,是因为其循环的次数是有限制的因此最终不会发生死循环;但是SPFA算法不可以,由于用了队列来存储,只要发生了更新就会不断的入队,因此假如有负权回路请你不要用SPFA否则会死循环。

    5. 由于SPFA算法是由Bellman_ford算法优化而来,在最坏的情况下时间复杂度和它一样即时间复杂度为 O(nm),假如题目时间允许可以直接用SPFA算法去解Dijkstra算法的题目。

  • ⭐️限制

    没有负环就可以使用,99%的题没有负环,但是可以用来判断负环

    阴险的出题人会把SPFA卡掉,使达到最坏O(nm), 网格图可能会卡
    在这里插入图片描述

  • ⭐️步骤

    由Bellman-Ford算法用宽搜做优化

    queue <- 1
    while  queue 不空 //队列里面放的是 待更新的点  
        t=q.front
        q.pop()
        
        更新t的所有边 t-w->b
        queue <- b
    

4.2图示

给定一个有向图,如下,求A~E的最短路。

1.png

源点A首先入队,然后A出队,计算出到BC的距离会变短,更新距离数组,BC没在队列中,BC入队

2.png

B出队,计算出到D的距离变短,更新距离数组,D没在队列中,D入队。然后C出队,无点可更新。

3.png

D出队,计算出到E的距离变短,更新距离数组,E没在队列中,E入队。

4.png

E出队,此时队列为空,源点到所有点的最短路已被找到,A->E的最短路即为8

5.png

4.2模板题

  • ⭐️题目

    链接: 851. spfa求最短路 - AcWing题库

    给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数

    请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 impossible

    数据保证不存在负权回路。

    输入格式

    第一行包含整数 n 和 m。

    接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。

    输出格式

    输出一个整数,表示 1 号点到 n 号点的最短距离。

    如果路径不存在,则输出 impossible

    数据范围

    1≤n,m≤ 1 0 5 10^5 105 ,
    图中涉及边长绝对值均不超过 10000。

    输入样例:

    3 3
    1 2 5
    2 3 -3
    1 3 4
    

    输出样例:

    2
    
  • ⭐️题解

    #include<bits/stdc++.h>
    using namespace std;
    
    const int N=1e5+10;
    
    int n,m;
    int h[N],e[N],ne[N],w[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;
                    }
                }
            }
        }
        if(dist[n]>0x3f3f3f3f/2) return 0x3f3f3f3f/2+1; //记住有负权边的最短路,最后都这样处理
        return dist[n];
        
    }
    
    int main()
    {
        scanf("%d%d",&n,&m);
        
        memset(h,-1,sizeof h);  //链表初始化
        
        for(int i=0;i<m;i++)
        {
            int a,b,w;
            scanf("%d%d%d",&a,&b,&w);
            add(a,b,w);
        }
        
        int t=spfa();
        
        if(t>0x3f3f3f3f/2) puts("impossible");
        else printf("%d",t);
        
        return 0;
    }
    

4.4判断负环

  • ⭐️思路

    cnt记录每个点到起点的边数,当cnt[i] >= n 表示出现了边数>=结点数,必然有环,而且一定是负环

    根据抽屉原理,可得。

    抽屉原理:如果每个抽屉代表一个集合,每一个苹果就可以代表一个元素,假如有n+1个元素放到n个集合中去,其中必定有一个集合里至少有两个元素。

  • ⭐️题目

    链接: AcWing 852. spfa判断负环 - AcWing

    给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数

    请你判断图中是否存在负权回路。

    输入格式

    第一行包含整数 n 和 m。

    接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。

    输出格式

    如果图中存在负权回路,则输出 Yes,否则输出 No

    数据范围

    1≤n≤2000,
    1≤m≤10000,
    图中涉及边长绝对值均不超过 10000。

    输入样例:

    3 3
    1 2 -1
    2 3 4
    3 1 -4
    

    输出样例:

    Yes
    
  • ⭐️题解

    #include<bits/stdc++.h>
    using namespace std;
    
    const int N=2010,M=1e4+10;
    
    int n,m;
    int h[N],e[M],w[M],ne[M],idx;
    int dist[N],cnt[N]; 
    //dist 存的是当前从1号点到n号点的长度
    //cnt 表示从1到x的最短路径中经过的点数
    bool st[N];
    
    void add(int a,int b,int c)
    {
        e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
    }
    
    bool spfa()
    {
        queue<int> q;  //这里不用初始化距离,因为咱不是求最短路滴
        
        for(int i=1;i<=n;i++)  //把所有的点放进队列里面去,可能存在负环,无法到达起点1,所有遍历所有点
        {
            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;  //当前到该点的边数+1
                    if(cnt[j]>=n) return true;  //cnt>=n,说明有点走了两遍,一定存在负环
                    
                    if(!st[j])
                    {
                        st[j]=true;  //入队
                        q.push(j);
                    }
                }
            }
        }
        return false;  //走到这了,函数还没结束,意味着边数一直小于结点数,不存在负环
    }
    
    int main()
    {
        scanf("%d%d",&n,&m);
        
        memset(h,-1,sizeof h);
        
        while(m--)
        {
            int a,b,w;
            scanf("%d%d%d",&a,&b,&w);
            add(a,b,w);
        }
        
        if(spfa()) puts("Yes");
        else puts("No");
        
        return 0;
    }
    

5.Floyd算法

5.1思想

不能应用于含负权回路的图

d[i][j] //邻接矩阵,存的是 i到j 的最短路问题
    
for(k=1;k<=n;k++)
    for(i=1;i<=n;i++)
        for(j=1;j<=n;j++)
            d[i][j]=min(d[i][j],d[i][k]+d[k][j])  //更新

5.2模板题

  • ⭐️题目

    链接: 854. Floyd求最短路 - AcWing题库

    给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,边权可能为负数。

    再给定 k 个询问,每个询问包含两个整数 x 和 y,表示查询从点 x 到点 y 的最短距离,如果路径不存在,则输出 impossible

    数据保证图中不存在负权回路。

    输入格式

    第一行包含三个整数 n,m,k。

    接下来 m 行,每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。

    接下来 k 行,每行包含两个整数 x,y,表示询问点 x 到点 y 的最短距离。

    输出格式

    共 k 行,每行输出一个整数,表示询问的结果,若询问两点间不存在路径,则输出 impossible

    数据范围

    1≤n≤200,
    1≤k≤ n 2 n^2 n2
    1≤m≤20000,
    图中涉及边长绝对值均不超过 10000。

    输入样例:

    3 3 2
    1 2 1
    2 3 2
    1 3 1
    2 1
    1 3
    

    输出样例:

    impossible
    1
    
  • ⭐️题解

    #include<bits/stdc++.h>
    using namespace std;
    
    const int N=210,INF=1e9;
    
    int n,m,k;
    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,&k);
        
        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,w;
            scanf("%d%d%d",&a,&b,&w);
            d[a][b]=min(d[a][b],w);
        }
        
        floyd();
        
        while(k--)
        {
            int a,b;
            scanf("%d%d",&a,&b);
            if(d[a][b]>INF/2) puts("impossible");
            else printf("%d\n",d[a][b]);
        }
        
        return 0;
    }
    

五种算法总结🎈

⭐️Dijkstra-朴素 O(n^2)

  1. 初始化距离数组, dist[1] = 0, dist[i] = inf;
  2. for n次循环 每次循环确定一个min加入S集合中,n次之后就得出所有的最短距离
  3. 将不在S中dist-min的点->t
  4. t->S加入最短路集合
  5. 用t更新到其他点的距离

⭐️Dijkstra-堆优化 O(mlogn)

  1. 利用邻接表,优先队列
  2. 在priority_queue[HTML_REMOVED], greater[HTML_REMOVED] > heap;中将返回堆顶
  3. 利用堆顶来更新其他点,并加入堆中类似宽搜

⭐️Bellman-ford O(nm)

  1. 注意连锁想象需要备份, struct edge { int a , b , c } Edge[M];
  2. 初始化 dist , 松弛 dist [x.b] = min ( dist [x.b] , backup [x.a] + x.w ) ;
  3. 松弛 k 次,每次访问 m 条边,有边数限制

⭐️SPFA O(n)~O(nm)

  1. 利用队列优化仅加入修改过的地方
  2. for k次
  3. for 所有边利用宽搜模型去优化 Bellman-ford 算法
  4. 更新队列中当前点的所有出边

⭐️Floyd O( n 3 n^3 n3)

  1. 初始化d
  2. k, i, j 去更新d

部分解释及图示转自acwing
Alt

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

指针不指南吗

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值