Dijkstra迪杰斯特拉算法的介绍(分为朴素dj和堆优化版dj),包含模板总结(必掌握)与具体例题应用

(🔺)朴素dijkstra迪杰斯特拉算法

  • 时间复杂度分析
    • 寻找路径最短的点:O(n²)
    • 加入集合S:O(n)
    • 更新距离:O(m)
    • 所以总的时间复杂度为O(n²)
    • 精确:时间复杂度 O(n²+m), n表示点数,m表示边数
  • 所有边若是正的,就不会有自环;重边保留长度最短的边即可

朴素dijkstra算法的模板

  • 距离指1号点到当前最短路的距离
int g[N][N];  // 稠密图用邻接矩阵存储每条边
int dist[N];  // 存储1号点到每个点的最短距离
bool st[N];   // 存储每个点的最短路是否已经确定(当前已确定其最短路的点,放置st[]中)

// 求1号点到n号点的最短路,如果不存在则返回-1
int dijkstra()
{
//①初始化距离
    dist[1] = 0;//第一步仅确定起点dist,其余的都初始为一个大值
    memset(dist, 0x3f, sizeof dist);

//②for循环(保证每次迭代都能确定一个点的最短距离)
    for (int i = 0; i < n - 1; i ++ )
    {
	    // a.t←在还未确定最短路的点中(即不在s[]中的),寻找距离最小的点 ==>n²
        int t = -1;     
        for (int j = 1; j <= n; j ++ )
            if (!st[j] && (t == -1 || dist[t] > dist[j]))
                t = j;

        // b.s←t(n次),用t更新其他点的距离(m次)
        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];
}

朴素dijkstra算法的例题

题意分析:
  • 重边情况的解决:在各边输入到g数组中时,就只保留最短的边
    • 见分析点④处g[x][y]=min(g[x][y],z);
  • 自环情况的解决:自环的处理直接忽略,因为求最小值的过程之中,如果出现了自环是不会纳入最小值
    • 对于dist[j] = min(dist[j], dist[t] + g[t][j]);自环意味t和j同一个,所以这一步相当于dist[j] = min(dist[j], dist[j] + g[j][j]);==>dist[j]必然小于dist[j] + g[j][j] ==>故说自环不会纳入最小值
代码分析:
  • 分析点①:为什么每次要选择“距离起点最近的未访问节点”呢?

    • 因为如果当前节点到起点的距离比已知的最短距离更长,那么从起点到当前节点的路径一定不是最短路径,因此没有必要继续扩展该节点。
  • 分析点②:

    • dist[t] + g[t][j]:是用1到t的距离,加上当前边(t→j,故g[t][j]),来更新从1到j其距离
    • 请添加图片描述
  • 分析点③:

    • memset是按字节,int共4字节,所以可用0x3f,每个字节刚好能0x3f3f3f3f
    • dist[n] == 0x3f3f3f3f处必须写完整0x3f3f3f3f,否则会出错
  • 分析点④:处理重边情况,g数组保留重边里最短的边

  • 分析点⑤:最先手动处理dist[1]=0的巧妙之处,相当是多米多骨牌里推倒第一个牌。

    • 其保证了在第一轮for循环中,步骤a中第一次确认的数是“1”.
    • ==>从而通过dist[j] = min(dist[j], dist[t] + g[t][j]);(对第一轮而言即dist[j] = min(无穷大, g[1][j]);),只有与1有边的(g[1][j]有值的),才会更新,其余仍是无穷大.
    • ==>故下一轮保证了是从1指向的几个数中,找dist最小的一个,进一步确定一轮最短距离.
    • ==>从而自动地连续且有序地更新dist,确定n个数的最小距离
  • 分析点⑥: 能走到这一步意味着st[t]=true,也就是st[n]=true.而st的意义正是固定一个值的最短距离是否确定;既然题目要求的n最短距离已找到,变可直接break,输出结果.

具体代码部分
#include<iostream>
#include<cstring>
using namespace std;

const int N = 510;

int n, m;
int g[N][N];    //稠密阵用邻接矩阵存储各边(初始化为无限大)
int dist[N];    //用于记录每一个点距离第一个点的距离(初始化为无限大)
bool st[N];     //用于记录该点的最短距离是否已经确定(默认初始化为0)

int Dijkstra()
{
    //①初始化距离
    memset(dist, 0x3f, sizeof dist);     //初始化距离(0x3f代表无限大)
    dist[1] = 0;  //第一个点到自身的距离为0;分析点⑤

    //②for循环n次,每次确定一个数,并更新一轮最短距离
    for (int i = 0; i < n; i++)      //有n个点所以要进行n次 迭代
    {
        int t = -1;       //t存储当前访问的点

		//a.确定数。要求如下:
	        //①最短距离仍未确定:st=false
	        //②满足①,同时dist最短(分析点①)
        for (int j = 1; j <= n; j++) //这里的j代表的是从1号点开始
        {
            //t==-1(表刚经历一次更新,先临时确定首个st[j]未走过的值为t);
            //若碰到更短的dist(dist[t] > dist[j])就更新t
            if (!st[j] && (t == -1 || dist[t] > dist[j]))
                t = j;
        }
	
        //if(t==n) break;//分析点⑥:说明找到了n,可以提前break
        
        //b.确认数后,更新对应st状态
        st[t] = true; 

        //c.依次再更新一轮每个点最短路径
        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和n是不连通的),即不存在最低路径
    }
    else {
        return dist[n];
    }
}

int main()
{
    //n:找1~n的最短路径;m:构建m条边的关系
    scanf("%d%d", &n, &m);

    //初始化图:因为是求最短路径,故每个点初始为无限大
    memset(g, 0x3f, sizeof g);

    while (m--)
    {
        int x, y, z;
        scanf("%d%d%d", &x, &y, &z);
        //g[x][y]记录x→y的边长z值
        g[x][y]=min(g[x][y],z);   //分析点④:min处理重边情况,保留最短的一条边
    }

    //输出1~n最短路径的答案
    int t = Dijkstra();
    printf("%d\n", t);

    return 0;
}

对学习算法的阶段性反思总结

  • 在学习本章算法时,初次接触一些算法,由于不理解与未知,我容易产生的恐惧厌学的逃避心理,如下总结一套自己的处理方式
    • (用于自己学习其他算法的框架,欢迎大家有所取舍地参考或分享自己处理方式):
  • ①先大致通过图解别人是怎么一步步处理的
  • ②再对应代码,理解哪些步骤是对应哪些代码的
    • 🔺棘手事同时引发心理问题的关键时刻
    • 到这一步常常会发现很多有不明白的地方,即使前面两步觉得自己已经理解处理方式了,但具体落实到代码上,细节或巧思设计还是会产生很多"为什么这么处理?"等不解。
    • 所以也是最容易产生“我不行/算了吧/先跳过”等逃避的想法。
    • 但其实已经到了最关键的地方,切勿逃避想着休息,而应该采取“一鼓作气再而衰三而竭”的战略对策
      • 但一鼓作气的方式也容易引起因精神紧张焦虑而钻牛角尖,处理方式:
        • 措施:疑惑与不解不要揉成杂乱无章的毛线团,而是选择深吸一口气,或者喝杯温水放松一下,暂时放空思路。
        • 心理:要明白越用力反而越会缠绕成死结,放松抖几下,反而毛线团容易自己散开。拒绝自我怀疑的焦虑,谨记焦虑解决不了问题!!
  • ③对于代码不理解部分,有以下几种可能
    • a.可能是一些前置知识的不足导致的==>可以先看看讨论区,容易发现别人或许也存在相同的疑虑,容易找到解答。
    • b.可能是因为对代码与思路的对应上有所偏差,理解错误导致的 ==> 对于此的处理方式,自己根据测试用例画图(因为我的情况是我比较依赖图示,脑子里没图很难理解),自己画不出图可以看看b站相关视频(大多能找到画的很好的图,甚至有动图)可以发现是不是什么细节代码对应思路的某部分理解错了?还是题意没搞清楚?;或者把正确代码放入vs下调试走一遍,看看人家正确的是怎么走的。
  • ④自己写一遍代码(不能照抄,但允许写卡顿的时候,并清楚下一步思路怎么走,只是代码细节不确定的地方可以看一眼正确代码)
    • 度过了③之后,其实已经掌握了大致,能保证思路是真的理解了。
    • 但不能保证能写正确代码(即不能保证用编程语言翻译出解题思路),所以一定要亲手写一遍,会发现有很多细节其实被漏掉了(比如有的for循环从1开始有的从0开始;又如本题中的dist[n] == 0x3f3f3f3f,之前memset写习惯0x3f,也通过此对memset字节读取有了更正确的理解。)
  • ⑤上手写AC后,最后可以再思考挖掘其中的拓展信息
    • a.代码中一些巧妙的设计,它是怎么在代码易懂的前提下简化代码的?
      • 如本题中的分析点⑤
    • b.对代码有了完整的了解后,根据体会是否能用人类的语言感性地、形象化地描述这个算法 ==> 利于记忆,也让算法变得鲜活有趣
      • 如对于分析点⑤:我的体悟,像推倒多米诺骨牌的第一张牌,只有dist[1]=0是我手动设置的,其余后面的都顺着我写的代码(我预先排列好的牌阵),一一倒下(一一记录更新dist数组中)
      • 又如[[1.基础算法#二维差分的例题]]中:我对差分的体悟,复刻前缀和数组同时,对于差分也是塑造、找回完整自我的过程。
      • 又如[[2.数据结构#KMP]]中:网友提供的体悟“一个人能走得多远,不在于他在顺境时能走得多快,而在于他在逆境时多久能找到曾经的自己。”
  • ⑥不放心自己是否理解,就睡前回忆下大致框架
    • 以前听他人提供过这样的思路,但一直没动力,觉得费事,就是懒。但其实若是碰上真心想学会掌握清楚的知识,我发现自己也能睡前去自觉地做这样一个回顾。
    • 心态上的转变就是:怕自己这么费事好像能终于掌握七七八八的知识,唯恐它丢了。自己视若珍宝的东西总是会习惯性地、有事没事看看,确保它还在、没丢。
    • 为了一种安全感,也因为一种不安全感,所以这样的心态会一定程度地打压抗拒和犯懒的心理。
  • 其他:焦虑的时候,想找人聊天解压或排解寻找建议 ==> 可以找chatGPT
    • ①chatGPT挺会安慰人的。
    • ②它说话提供的建议1.2.3…地列出来,能让我主动去分析问题。
      • 即使它给的学习方法等参考思路,比较泛泛而谈,但这问题不大。
      • 因为我的目的并不在于它提供给我多好或多正确的建议,能让我照搬照抄,而是在于通过它的理性表述,冷静下来,从而以“取舍它提供的建议”这一步作为开始,调动自己的思辨力主动去行动起来
      • 有用的行动不仅助于推动问题的解决,也能缓解焦虑的消极心态。
    • 如此,不必担心给朋友输送负面能量,或者聊着聊着学习的魂就飘到玩乐上了,或者朋友没时间等等情况
      请添加图片描述

(🔺)堆优化版dijkstra迪杰斯特拉算法

  • 时间复杂度 O(mlogn), n表示点数,m表示边数

  • 优化版其实就是用堆,堆朴素版dijkstra进行优化。如下是时间复杂度分析:

    • ①寻找路径最短的点:
      • 在朴素dijkstra算法中,效率最低的一步是遍历找距离最小的点(O(n²))==> 而该步目的是“在一堆树中找最短的数”,故可用数据结构中的解决。用堆优化后每次循环时间复杂度为O(1) ==> 故本次总计算量为n次
    • ②加入集合s:s←t==>计算量为n次
    • ③更新距离:但同时,由于采用堆的方式,所以在“用t更新其他点的距离”这步也会采用堆。而在堆当中修改一个数的时间复杂度是logn,修改m次==> 故这步计算量为mlogn
    • 故堆优化版最大计算量为mlogn
  • 用堆实现的方式

    • ① 手写堆 --> 时刻保证堆中有n个数
    • ② C++库中的优先队列(一般直接用优先队列);python中的set
      • [[章11.stack和queue#六、优先级队列priority_queue]]优先队列不支持修改任意一个元素的操作,它的修改操作是每次往队列里插入新的数
      • 优势:方便不用手写堆了;
      • 劣势:元素可能冗余,有m个(时间复杂度变成mlogm;但由于m≤n²,logm≤logn²,即logm≤2logn ==> 故logm和logn其实是同个级别,它俩时间复杂度是一样的 ==>也因此统一说堆优化版dijkstra时间复杂度为mlogn)

堆优化版dijkstra的模板

typedef pair<int, int> PII;

int n;      // 点的数量
int h[N], w[N], e[N], ne[N], idx;       // 邻接表存储所有边
int dist[N];        // 存储所有点到1号点的距离
bool st[N];     // 存储每个点的最短距离是否已确定

// 求1号点到n号点的最短距离,如果不存在,则返回-1
int dijkstra()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    priority_queue<PII, vector<PII>, greater<PII>> heap;
    heap.push({0, 1});      // first存储距离,second存储节点编号

    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] > distance + w[i])
            {
                dist[j] = distance + w[i];
                heap.push({dist[j], j});
            }
        }
    }

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

堆优化版dijkstra的例题

  • 例题:850. Dijkstra求最短路 II - AcWing题库

  • (以下分析点对应序号具体标记在代码展示部分)

  • 849 Dijkstra求最短路 I - AcWing题库不同的在于:

    • ①本题的权重是非负数,比上一种多了权重为0的情况。
    • n,m∈[1,1.5*10^5] > n~m>稀疏图==>邻接表
  • 思路

    • ①1号点距离初始化为0(dist[1]=0),其余点初始化为无穷大
    • ②将1号点放入堆中(heap.push({0, 1});
    • ③不断循环(直到堆空),更新最短距离。每次循环操作如下:
      • 确认数后 ==> 弹出堆顶,并标记已找到该点最短距离 ==>
  • 图解

    • 举例:图示请添加图片描述
    • h[]数组及节点w、e、ne值图示

    请添加图片描述

  • 代码分析

    • 分析点①~②:重边的处理过程,假设1->2有权重为2和3的边,再遍历到点1的时候2号点的距离会更新两次放入堆中==>因此堆中会有很多冗余的点,但是在弹出的时候还是会弹出最小值2+x(x为之前确定的最短路径)==> 并标记st为true,所以下一次弹出3+x会continue不会向下执行。
    • 分析点②:continue若距离已经确定(已经出现过ver该点,说明这个点是冗余备份),就跳过该行以下所有步骤(不必处理该点),直接再进入下次while循环
      • 由于小根堆即使能直接找到dist最短的点,但附带的不足是会产生冗余点,所以仍需st标志状态
      • 进一步思考continue的意义:
        • 正因为dj算法保证了先走到的永远是dist最短的==>即它的目的就是保证先走出图示中红色的路线
        • 为什么continue?
          • 因为1 3 122 3 9即使都比2 4 3更早存入小根堆中,但由于一直走小根堆里的top一直先走红色的路(即4 3 4虽然晚到,但它dist小,所以提前先走它),前俩者就一直被留在堆里,没办法走.
          • 等到轮到它俩的时候,4 3 4这条最短的路已经走到3号节点了 ==> 3号节点最短路径确定,st[3]=true ==> continue能保护在轮到1 3 122 3 9它俩走的时候,它们也没办法影响dist[3]了,因为3号节点已经被固定了
          • 个人体悟:只要优势足够大,后来者依旧拥有优先的特权.-- 优先队列模拟实现小根堆
    • 分析点③:依次插入距离和节点编号。顺序不能倒,pair排序时是先根据first,再根据second。
      • first:小根堆是根据距离来排的 ==> 所以first要先存距离
      • second:从堆中拿出来的时候要知道知道这个点是哪个点,从而更新邻接点 ==> 故第二个变量要存点编号
    • 分析点④:本题由于节点数量n和边数m的范围相同,故可以写作int h[N], w[N], e[N], ne[N], idx;,但若n和m不同的话,要用链式数组处理,正确应写作int h[N], e[M], ne[M], w[M], idx;
    • 分析点⑤:小根堆优先队列规定写法格式。
      • priority_queue默认大根堆,其后加vector<类型>greater<类型>可以设置成小根堆
      • 之前要求写法greater<PII> >,即需要在greater<PII>后加空格,避免被识别作位运算右移符号,但C++11已修改该问题。
    • 分析点⑥:此项for循环用于更新ver所指向的节点距离,更新后入堆(以待下轮堆中确定dist最短的数)
      • 为什么只有ver指向的节点更新最短路径呢?==> 因为只有被ver指向的才有对应的w[i],才可能有更短的距离
      • 而这些节点都被邻接表记录起来,不必像上题邻接矩阵这步从头一一循环。
        • 也正因为邻接表记录起来,能在一个链里依次找到所有ver指向的节点,其中也包含着重边的冗余项==>dist[data] > dist[ver] + w[i]就能保证dist只记录冗余项中最短的权重边(长的权重边进入不了if语句,无法push到heap中)。
  • 具体代码部分

//y总代码参考及自我理解注释
#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>//堆的头文件

using namespace std;

typedef pair<int, int> PII;//堆里存储距离和节点编号

const int N = 1e6 + 10;//或1.5*1e5 + 10也可

int n, m;//节点数量和边数
int h[N], w[N], e[N], ne[N], idx;//分析点④;邻接矩阵存储稀疏图;w[]存权重
int dist[N];//存储距离
bool st[N];//存储状态(true说明该点最短路径已经确定)

void add(int a, int b, int c)//分析点①a→b,权重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});//分析点③:依次插入距离和节点编号;把1号点放进去再更新其他点(顺序不可颠倒,因为pair是按first排序的)

    while (heap.size())//队列不空
    {
        //每次取距离源点最近的点
        auto t = heap.top();
        heap.pop();

        //distance:源点距离ver的距离;ver:节点编号
        int  distance = t.first,ver = t.second;
        
        if (st[ver]) continue;//分析点②
        st[ver] = true;

        //分析点⑥:更新ver所指向的节点距离,并存入堆
        for (int i = h[ver]; i != -1; i = ne[i])
        {
            //j存储编号
            int data = e[i];
            if (dist[data] > dist[ver] + w[i])
            {
                dist[data] = dist[ver] + w[i];
                heap.push({dist[data], data});//距离变小,则入堆
            }
        }
    }

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

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

    int t=dijkstra();
    printf("%d\n",t);

    return 0;
}
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值