朴素Dijkstra
食用指南:
对该算法程序编写以及踩坑点很熟悉的同学可以直接跳转到代码模板查看完整代码
只有基础算法的题目会有关于该算法的原理,实现步骤,代码注意点,代码模板,代码误区的讲解
非基础算法的题目侧重题目分析,代码实现,以及必要的代码理解误区
题目描述:
-
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,所有边权均为正值。
请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 −1。
输入格式
第一行包含整数 n 和 m。
接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。输出格式
输出一个整数,表示 1 号点到 n 号点的最短距离。
如果路径不存在,则输出 −1。数据范围
1≤n≤500,
1≤m≤105,
图中涉及边长均不超过10000。输入样例:
3 3
1 2 2
2 3 1
1 3 4
输出样例:
3 -
题目来源:https://www.acwing.com/problem/content/851/
题目分析:
- 图论:n点 & m边,n<500 ,m < 105 -> 有向稠密图 -> 邻接矩阵
- 边的权重:x y两点之间边长 z,求最短路长 -> 不可BFS
- 问题确定:求1号点到n号点的最短路长 -> 单源最短路径
- 所有边权为正值 -> 迪杰斯特拉算法
- 总结:基于邻接矩阵的Dijkstra算法
算法原理:
模板算法:
Dijkstra:
1. 适用情况:
- 单源最短路径:从指定一点到其他所有点的最短路径
- 图中边权值均为正:
- 稠密图:边数 是 点数的平方量级
2. 存储形式:
- 确定集s[N]:所有已经确定最短路径的图中节点,加入集合s[N]
- 距离集dis[N]:存储其余点到指定起点的现有最短距离
- 初始化:
未选择起点时,
确定集s[N]为空,初始化为不存在节点的序号
距离集dis[N]为无穷,初始化为难以达到任何点 - 选择单源/起点:
确定求从x号点到其他点的距离时,
将x号点到本身的距离dis[x] = 0;
虽然此时s[N]中可以加入x点,但是为了代码整体性,我们让接下来的循环帮我们干这件事
3. 三大步骤:
- 择点:每次从dis[N]中选择到单源距离最近,且未加入s[N]的点,加入s[N]
- 松弛:每次s[N]中新加入一点,利用该点尝试缩短与该点连通点到单源的距离
- 循环迭代上述两步,每迭代一次,都能确定单源到未确定的一点的最短距离
4. 模拟过程:
-
明确:包括起点在内,所有点的最短距离确定都是通过 “择点” + “松弛” 循环迭代而来的,起点其实也不例外
-
模拟:给定一图如下:
-
现指定1号节点为单源,求其余点到1号点的最短路径。
先将1号点到自己的距离dis[1]初始化为0,
-
一轮择点&松弛后:
-
二轮择点&松弛后:
-
三轮择点&松弛后:
-
四轮择点&松弛后:
-
五轮择点&松弛后:
- 最终5个点到单源1号点的最短距离为:
dis[1] = 0; dis[2] = 2; dis[3] = 5;
dis[4] = 6; dis[5] = 9; - 通过模拟过程,我们可以发现以下4个规律:
-
有几个节点就有几轮循环 择点 + 松弛
-
每个节点在加入s[N]中时,将其发起的边所连节点松弛一遍
-
初始化仅仅将dis[单源]=0;未将单源加入s[]
因为加入s[N]后必须松弛
如果你想手动为单源点所发出的边进行松弛,那你可以初始化就dis[单源]=0;s[tt++]=单源
-
反过来看,单源点也是因为dis[N]最小才被择点过程选中进入的s[N],不是因为是起点所以进入s[N]
-
5. 分析时间复杂度:
- 循环择点:图中有几个节点择几轮循环
- 每轮循环有两大步骤:
择最近点O(n) & 松弛该点发起的边O(n)
择最近点通过一次遍历图中所有未加入s[N]的节点
松弛点又经过了一次遍历所有与该点相连出边终点 - 一个大循环 套 两个小循环,时间复杂度共计O(2n2)
写作步骤:
1. 初始化:
- 密集图以邻接矩阵存储,不通两点arr[x][y] = +∞;
- 各点到单源距离dis[N],初始无择点,无择路,dis[N]均为正无穷,表示不通
- 确定集s[N],初始无择点,s[N] = 0;
- 单源自身dis[源] = 0;
2. 边长读入:
- 由于存在重边,故最短路径要在边输入时就选择短的那条
3. Dijkstra:
- 大循环:n轮
- 内部双循环:择最近点入s[N] & 利用该点更新距离
4. 输出最短距离:
- 单源到n号点若不连通,则dis[n]保持无穷
- 由于借助其余点可能略缩短到n号点距离
所以dis[n]可能在无穷基础上略微缩短 - 若最终dis[n]近于无穷,则说明单源到该点不连通,输出-1
- 若dis[n]是常数,则连通,输出距离dis[n]
代码实现:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 510;
int arr[N][N];
int dis[N];
bool s[N];
int n, m;
int dijkstra(int start){
memset(dis, 0x3f, sizeof(dis));
dis[start] = 0;
for(int i=0; i<n; i++){
//一,择点:
int t = -1;
for(int j=1; j<=n; j++){
//开始选一个未选组中点,之后不断和其他未选组中点作比较
if(!s[j]&&(t==-1||dis[t]>dis[j])){
t = j;
}
}
s[t] = 1;
//二,松弛:
for(int j=1; j<=n; j++){
//arr[N]初始化为无穷,保证了不连通未更新dis[j]
dis[j] = min(dis[j], dis[t]+arr[t][j]);
}
}
if (dis[n]>0x3f3f3f/2)
return -1;
else return dis[n];
}
int main(){
cin >>n >>m;
memset(arr,0x3f,sizeof(arr));
while(m--){
int x, y, z;
cin >>x >>y >>z;
arr[x][y] = min(arr[x][y], z);
}
int start = 1;
cout<< dijkstra(start);
return 0;
}
代码误区:
1. 单源起初就被加入s[N]了吗?
- 不是,单源其实和其他点一样
如果非要说起点特殊,就只是从一堆dis[N] = ∞ 中初始化出了一个dis[单源] = 0; - 图中所有的点都是因为到单源的距离最短
而在择点过程中被选中,确定为最短距离,加入s[N],更重要的是依据该点更新其余连接点的dis[N] - 如果你不将起点连接点的dis[N]手动更新一遍,那你还是乖乖将加入s[N]的任务交给循环吧
2. 自环影响最短路径吗?
-
自环只要不为负数,则不影响
-
而若边为负数,根本不存在最短路径,每多走一次自环,其余点距离减少一遍
-
从过程上来说
起点或其余点t被加入s[]后,遍历所有连接点j更新dis[j]
dis[j] = min(dis[j], dis[j]+arr[t][j]);由于自环边权>=0,所以dis[t] <= dis[t] + arr[t][t];
此处不影响dis[N]
则不影响下一次最近点的选取,对此后都无影响
3. 择点步骤的作用:
- 择点本身有两大作用:确定本点最短距离 & 更新其余连接点到单源最短路径,便于下次择点
- 所有点都是因为dis[i]最小而被择点进入确定集s[N],起点也不例外
4. 再提memset():
-
memset(起始地址,单字节值,地址长度);
-
注意int本身占据4个字节,而memset()按照单个字节填写
-
int a = 1;
memset(&a, -1, sizeof(a)); 则 a = -1;
memset(&a, 0, sizeof(a)); 则 a = 0;
这是由于0补码全是0,-1补码全是1但memset(&a, 1, sizeof(a));则a != 1;
这是由于1补码是0000 0001
四个0000 0001放在一起则是0000 0001 0000 0001 0000 0001 0000 0001
是个非常大的int:20+28+216 + 224
本篇感想:
- Dijkstra本身很简单,就三句话:
择近点,松弛边,循环迭代 - 难点和易错点都在初始化
- 看完本篇博客,恭喜已登 《筑基境-中期》
距离登仙境不远了,加油