朴素Dijkstra与堆优化Dijkstra总结
1.朴素版dijkstra
算法
Dijkstra 的整体思路
即进行n(n为n的个数)次迭代去确定每个点到起点的最小值 最后输出的终点的即为我们要找的最短路的距离
按照这个思路除了需要存储图外(邻接矩阵)还需要存储这两个量:
dist[N] //用于存储每个点到起点的最短距离
st[N] //用于在更新最短距离时 判断当前的点的最短距离是否确定 是否需要更新
每次迭代的过程中我们都先找到当前未确定的最短距离的点中距离最短的点
int t=-1;//将t设置为-1 因为Dijkstra算法适用于不存在负权边的图
for(int j=1;j<=n;j++){
if(!st[j]&&(t==-1||dist[j]<dist[t])){//寻找还未确定最短路的点中路径最短的点
t=j;
}
}
通过上述操作当前我们的t代表就是剩余未确定最短路的点中 路径最短的点
而与此同时该点的最短路径也已经确定将该点标记
st[t]=true;
然后用这个点去更新其余未确定点的最短距离
for(int j=1;j<=n;j++){
dist[j]=min(dist[j],dist[t]+g[t][j]);
}
//思考:j如果从1开始的话 会不会影响之前已经确定的点的最小距离?
//不会 因为按照我们的Dijkstra算法的操作顺序 先确定最短距离的点的距离已经比后确定的要小 所以不会影响
进行n次迭代后最后就可以确定每个点的最短距离
然后再根据题意输出相应的 要求的最短距离
以下为AcWing
849. Dijkstra求最短路 I
题目来源:https://www.acwing.com/problem/content/851/
#include<algorithm>
#include<string.h>
#include<iostream>
using namespace std;
const int N=510;
int g[N][N];//为稠密阵所以用邻接矩阵存储
int dist[N];//用于记录每一个点距离第一个点(起点)的距离
bool st[N]; //用于记录该点的最短距离是否已经确定
int n,m;
int dijkstra(){
memset(dist,0x3f,sizeof dist);//初始化距离 0x3f代表无限大
dist[1]=0;//第一个点到自身的距离为0
for(int i=0;i<n;i++){//有n个点所以要进行n次 迭代
int t=-1; //t存储当前访问的点
for(int j=1;j<=n;j++){//这里的j代表的是从1号点开始
if(!st[j]&&(t==-1||dist[j]<dist[t])){
t=j;
}
}
st[t]=true;
for(int j=1;j<=n;j++){//依次更新每个点所到相邻的点路径值
dist[j]=min(dist[j],dist[t]+g[t][j]);
}
}
if(dist[n]==0x3f3f3f3f)return -1;//如果第n个点路径为无穷大即不存在最低路径
return dist[n];
}
int main(){
memset(g,0x3f,sizeof g);//初始化图 因为是求最短路径 所以每个点初始为无限大
scanf("%d%d",&n,&m);
while(m--){
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
g[a][b]=min(c,g[a][b]);//如果发生重边的情况则保留最短的一条边
}
cout<<dijkstra()<<endl;
return 0;
}
该段总结参考了作者Ni在Acwing
求最短路题中写的题解与总结
地址如下:https://www.acwing.com/solution/content/5806/
2.堆优化版dijkstra
算法
为什么要堆优化,首先看一下堆优化版和朴素版在时间复杂度上的区别
朴素版时间复杂度分析
寻找路径最短的点:O(n^2)
加入集合S:O(n)
更新距离:O(m)
堆优化版时间复杂度分析
寻找路径最短的点:O(n)
加入集合S:O(n)
堆的插入操作:O(log(n))
更新距离:O(mlog(n))
部分借鉴于:https://www.acwing.com/solution/content/6554/
朴素版时间复杂度O(n^2)
堆优化版时间复杂度O(mlog(n))
链式前向星
在学习堆优化版dijkstra算法之前首先需要知道什么是前向星
前向星的基本定义和实现可以看博主:水无垠的博客 C++链式前向星
利用前向星会有排序操作,如果用快排时间至少为O(nlog(n))
如果用链式前向星,就可以避免排序.
建立结构体:
struct Edge
{
int next;
int to;
int w;
};
edge[i].to 表示第i条边的终点
edge[i].next 表示与第i条边同起点的下一条边的存储位置
edge[i].w 为边权值
int head[];
另外还有一个数组head[],用来表示以i为起点的第一条边存储的位置
其实这里的第一条边存储的位置其实是 在以i为起点的所有边的最后输入的那个编号
head[]数组一般初始化为-1
memset(head,-1,sizeof head);
加边的add函数
void add(int x,int y,int w)
{
edge[cnt].w=w;
edge[cnt].to=y;
edge[cnt].next=head[x];
head[x]=cnt++;
}
初始化cnt=0;
模拟与解释
我们输入边的顺序为:
1 2
2 3
3 4
1 3
4 1
1 5
4 5
按照图和输入模拟一下过程
edge[0].to=2;edge[0].next=-1;head[1]=0;
edge[1].to=3;edge[1].next=-1;head[2]=1;
edge[2].to=4;edge[2].next=-1;head[3]=2;
edge[3].to=3;edge[3].next=0;head[1]=3;
edge[4].to=1;edge[4].next=-1;head[4]=4;
edge[5].to=5;edge[5].next=-1;head[1]=5;
edge[6].to=5;edge[6].next=4;head[4]=6;
很明显,head[i]保存的是以i为起点的所有边中编号最大的那个,而把这个当作顶点i的第一条起始边的位置
这样在遍历时是倒着遍历的,也就是说与输入顺序是相反的,不过这样不影响结果的正确性
比如以上图为例,以节点1为起点的边有3条,它们的编号分别是0,3,5 而head[1]=5
我们在遍历以x为起始位置的所有边的时候是这样的
for(int i=head[x];i!=-1;i=edge[i].next)
那么也就是说先遍历编号为5的边,也就是head[1],然后就是edge[5].next,也就是编号为3的边,然后就是edge[3].next,也就是编号为0的边,可以看出是逆序的
优先队列
优先队列具有队列的所有特性,包括基本操作,只是在这基础上添加了内部的一个排序,它本质是一个堆实现的
和队列基本操作相同:
- top 访问队头元素
- empty 队列是否为空
- size 返回队列内元素个数
- push 插入元素到队尾 (并排序)
- emplace 原地构造一个元素并插入队列
- pop 弹出队头元素
- swap 交换内容
定义:priority_queue<Type, Container, Functional> Type 就是数据类型,Container 就是容器类型(Container必须是用数组实现的容器,比如vector,deque等等,但不能用 list。STL里面默认用的是vector),Functional 就是比较的方式,当需要用自定义的数据类型时才需要传入这三个参数,使用基本数据类型时,只需要传入数据类型,默认是大顶堆
//升序队列
priority_queue <int,vector<int>,greater<int> > q;
//降序队列
priority_queue <int,vector<int>,less<int> >q;
该段参考了博主吕白_的博客:c++优先队列(priority_queue)用法详解
堆优化版dijkstra的思路
堆优化版的dijkstra是对朴素版dijkstra进行了优化,在朴素版dijkstra中时间复杂度最高的寻找距离最短的点O(n^2)可以使用最小堆优化。
- 起始点的距离初始化为零,其他点初始化成无穷大。
- 将起始点放入堆中。
- 不断循环,直到堆空。每一次循环中执行的操作为:
弹出堆顶(与朴素版diijkstra找到S外距离最短的点相同,并标记该点的最短路径已经确定)。
用该点更新临界点的距离,若更新成功就加入到堆中。
以下为AcWing
850. Dijkstra求最短路 II
题目来源:https://www.acwing.com/problem/content/852/
#include<iostream>
#include<string.h>
#include<stdio.h>
#include<string.h>
#include<queue>
using namespace std;
const int N=150010;
typedef pair<int,int> PII;//<离起点的距离,节点编号>
int to[N],ne[N],w[N],idx;
int h[N];
int dist[N];
int st[N];
int n,m;
//在x节点之后插入一个y节点,权重为w
void add(int x,int y,int c){
w[idx]=c;
to[idx]=y;
ne[idx]=h[x];
h[x]=idx++;
}
int dijkstra(){
memset(dist,0x3f,sizeof dist);//所有距离初始化为无穷大
dist[1]=0;//1号节点距离为0
priority_queue<PII,vector<PII>,greater<PII> > heap;//建立一个小根堆 --优先队列
heap.push({0,1});//1号节点插入堆
while(heap.size()){
PII t=heap.top();//取出堆顶顶点
heap.pop();//并从堆中删除
int ver=t.second,distance=t.first; //取出节点编号和节点距离
if(st[ver])continue;//如果节点被访问过则跳过
st[ver]=true;
//遍历以ver为起始位置的所有边
for(int i=h[ver];i!=-1;i=ne[i]){
int j=to[i];//取出节点编号
if(distance+w[i]<dist[j]){
dist[j]=distance+w[i];
heap.push({dist[j],j});
}
}
}
if(dist[n]==0x3f3f3f3f)return -1;
return dist[n];
}
int main(){
memset(h,-1,sizeof h);//将h[]数组初始化为-1
scanf("%d%d",&n,&m);
for(int i=0;i<m;i++){
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
add(a,b,c);
}
cout<<dijkstra();
return 0;
}
该段总结参考了作者松鼠爱葡萄 在Acwing
求最短路题中写的题解与总结
地址:https://www.acwing.com/solution/content/14007/
小结:
总算是学习(总结)完了单元最短路中最基础的算法Dijkstra算法(该算法只能用于所有边权都为正数的图),下一篇应该是能够解决负权边问题的Bellman-Ford和SPFA算法了。
本人也是初学算法,在学习过程中深刻体会到了算法学习的不易,在个人总结的同时,也希望这篇文章能够帮助到和我一样在学习算法的朋友!要是发现文章中有写错的没写全的地方,也希望各路大佬能够指正呀!谢谢了!