(🔺)朴素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算法的例题
- 849. Dijkstra求最短路 I - AcWing题库
- (以下分析点对应序号具体标记在代码展示部分)
题意分析:
- 重边情况的解决:在各边输入到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]]中:网友提供的体悟“一个人能走得多远,不在于他在顺境时能走得多快,而在于他在逆境时多久能找到曾经的自己。”
- 如对于分析点⑤:我的体悟,像推倒多米诺骨牌的第一张牌,只有
- a.代码中一些巧妙的设计,它是怎么在代码易懂的前提下简化代码的?
- ⑥不放心自己是否理解,就睡前回忆下大致框架
- 以前听他人提供过这样的思路,但一直没动力,觉得费事,就是懒。但其实若是碰上真心想学会掌握清楚的知识,我发现自己也能睡前去自觉地做这样一个回顾。
- 心态上的转变就是:怕自己这么费事好像能终于掌握七七八八的知识,唯恐它丢了。自己视若珍宝的东西总是会习惯性地、有事没事看看,确保它还在、没丢。
- 为了一种安全感,也因为一种不安全感,所以这样的心态会一定程度地打压抗拒和犯懒的心理。
- 其他:焦虑的时候,想找人聊天解压或排解寻找建议 ==> 可以找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的例题
-
(以下分析点对应序号具体标记在代码展示部分)
-
与849 Dijkstra求最短路 I - AcWing题库不同的在于:
- ①本题的权重是非负数,比上一种多了权重为0的情况。
- ②
n,m∈[1,1.5*10^5]
> n~m>稀疏图==>邻接表
-
思路:
- ①1号点距离初始化为0(
dist[1]=0
),其余点初始化为无穷大 - ②将1号点放入堆中(
heap.push({0, 1});
) - ③不断循环(直到堆空),更新最短距离。每次循环操作如下:
- 确认数后 ==> 弹出堆顶,并标记已找到该点最短距离 ==>
- ①1号点距离初始化为0(
-
图解
- 举例:图示
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 12
和2 3 9
即使都比2 4 3
更早存入小根堆中,但由于一直走小根堆里的top一直先走红色的路(即4 3 4
虽然晚到,但它dist小,所以提前先走它),前俩者就一直被留在堆里,没办法走. - 等到轮到它俩的时候,
4 3 4
这条最短的路已经走到3号节点了 ==> 3号节点最短路径确定,st[3]=true
==> continue能保护在轮到1 3 12
和2 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中)。
- 也正因为邻接表记录起来,能在一个链里依次找到所有ver指向的节点,其中也包含着重边的冗余项==>
- 为什么只有ver指向的节点更新最短路径呢?==> 因为只有被ver指向的才有对应的
-
具体代码部分
//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;
}