前言
由于该篇讲的是Dijkstra算法的优化,所以不会从头讲一遍Dijkstra算法,但会有较为详尽的注释来方便记忆堆优化后的Dijkstra算法。
正文
首先呢,大家都知道Dijkstra算法是用于求最短路的,但是不经优化的Dijkstra算法的时间复杂度是O(n*n)的,可以说是比较容易爆掉,但是呢我们可以用堆这个东西对Dijkstra算法进行优化,从而将时间复杂度降为O((m+n)*logn),可以解决无负边权的最短路问题。而对于有负边权的图,则需要用的SPFA算法。而Floyed算法又非常的好背,并且Floyed算法时间复杂度较高,仅能用于解决多源最短路问题。所以说,对于无负边权的单源最短路问题,使用Dijkstra算法;对于有负边权的单源最短路问题,则使用SPFA算法;对于多源最短路问题,则使用Floyed算法。
Dijksta算法在最短路问题中是应用最为广泛的算法之一,非常值得记忆和理解,需要大家灵活使用。
Dijkstra算法的堆优化自然是要用到堆的,而实现堆自然就要用到优先队列,所以说我们在跑Dijkstra算法之前,需要进行如下操作(顺便把链式前向星加边直接写进来):
struct edge
{
int to,nxt,dis;
}e[MAXN<<1];
struct node//堆节点
{
int u,d;//u表示当前点的标号,d表示当前点对应的最短距离
bool operator < (const node &x) const//重载运算符,把“< ”重新定义,定义为d较大的数更小
{
return d>x.d;
}
};
int head[MAXN],tt=0;
int dis[MAXN];//dis[u]存储的是源点到点u的最小距离
void add(int x,int y,int z)
{
e[++tt].nxt=head[x];
head[x]=tt;
e[tt].to=y;
e[tt].dis=z;
}
其中用结构体实现链式前向星,用优先队列实现一个小根堆(这是显而易见的),dis数组则存储的是一个点到源点的最短距离。
那么该准备的东西准备好后,就可以开始跑Djlstra算法了,不过与以前的一样,我们要先处理dis数组。设源点为s,则有如下操作:
for(int i=1;i<=n;i++)
dis[i]=INF;//除了初始点外其余点全部设为正无穷
dis[s]=0;//初始点设为0
然后呢,由于我们使用的是堆优化,所以我们自然也是要给堆内的元素进行赋值:
priority_queue<node>q;//用优先队列实现堆
q.push((node){s,0});//s表示起点,0表示赋的初始最短路径
这里队列q新加进去的{s,0}元素分别对应准备工作中处理堆节点部分的u和d。
总之这两部分就是初始化,将源点s的信息处理一下。
接下来,就要开始跑Dijkstra算法了,注释写的已经比较清楚了:
while(!q.empty())
{
node first=q.top();//由于队列q为优先队列,所以first即为当前队列中的最小值,也就是正在处理的路径中的最小值
q.pop();//处理了当前的最小值,直接出队
int u=first.u;
int d=first.d;
if(d!=dis[u])//每次松弛操作后都要删掉对应节点,所以需要判断是否删除过一个点来简化操作
continue;
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].to;//遍历与点u相连的点
int w=e[i].dis;//点u到点v的距离
if(dis[v]>dis[u]+w)//dis[u]+w就是一个与u相连的点v与点u间的距离,若当前点v与相连的点的最短距离较大,则将其更新为
{
dis[v]=dis[u]+w;//更新距离
q.push((node){v,dis[v]});//由于点v被更新,所以当前的v所在路径更短,那么就让点v和对应的距离入队
}
}
}
所以整个Dijkstra的过程就是这样的:
void Dijkstra()
{
for(int i=1;i<=n;i++)
dis[i]=INF;//除了初始点外其余点全部设为正无穷
dis[s]=0;//初始点设为0
priority_queue<node>q;//用优先队列实现堆
q.push((node){s,0});//s表示起点,0表示赋的初始最短路径
while(!q.empty())
{
node first=q.top();//由于队列q为优先队列,所以first即为当前队列中的最小值,也就是正在处理的路径中的最小值
q.pop();//处理了当前的最小值,直接出队
int u=first.u;
int d=first.d;
if(d!=dis[u])//每次松弛操作后都要删掉对应节点,所以需要判断是否删除过一个点来简化操作
continue;
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].to;//遍历与点u相连的点
int w=e[i].dis;//点u到点v的距离
if(dis[v]>dis[u]+w)//dis[u]+w就是一个与u相连的点v与点u间的距离,若当前点v与相连的点的最短距离较大,则将其更新为
{
dis[v]=dis[u]+w;//更新距离
q.push((node){v,dis[v]});//由于点v被更新,所以当前的v所在路径更短,那么就让点v和对应的距离入队
}
}
}
}
最后呢放一道模板题P4779 【模板】单源最短路径(标准版),直接套优化后的Dijkstra即可。下面放上我的AC代码:
#include<iostream>
#include<cstdio>
#include<cmath>
#include<string>
#include<cstring>
#include<algorithm>
#include<cassert>
#include<stack>
#include<queue>
#include<vector>
#include<map>
#include<cstdlib>
using namespace std;
#define ll long long
#define ull unsigned long long
int read()
{
int now=0,nev=1;
char c=getchar();
while(c<'0' || c>'9')
{
if(c=='-')
nev=-1;
c=getchar();
}
while(c>='0' && c<='9')
{
now=(now<<1)+(now<<3)+(c&15);
c=getchar();
}
return now*nev;
}
const int MAXN=1e5+10;
const int INF=1e9;
int n,m,s;
int a[MAXN];
struct edge
{
int to,nxt,dis;
}e[MAXN<<1];
struct node//堆节点
{
int u,d;//u表示当前点的标号,d表示当前点对应的最短距离
bool operator < (const node &x) const//重载运算符,把“< ”重新定义,定义为d较大的数更小
{
return d>x.d;
}
};
int head[MAXN],tt=0;
int dis[MAXN];//dis[u]存储的是源点到点u的最小距离
void add(int x,int y,int z)
{
e[++tt].nxt=head[x];
head[x]=tt;
e[tt].to=y;
e[tt].dis=z;
}
void Dijkstra()
{
for(int i=1;i<=n;i++)
dis[i]=INF;//除了初始点外其余点全部设为正无穷
dis[s]=0;//初始点设为0
priority_queue<node>q;//用优先队列实现堆
q.push((node){s,0});//s表示起点,0表示赋的初始最短路径
while(!q.empty())
{
node first=q.top();//由于队列q为优先队列,所以first即为当前队列中的最小值,也就是正在处理的路径中的最小值
q.pop();//处理了当前的最小值,直接出队
int u=first.u;
int d=first.d;
if(d!=dis[u])//每次松弛操作后都要删掉对应节点,所以需要判断是否删除过一个点来简化操作
continue;
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].to;//遍历与点u相连的点
int w=e[i].dis;//点u到点v的距离
if(dis[v]>dis[u]+w)//dis[u]+w就是一个与u相连的点v与点u间的距离,若当前点v与相连的点的最短距离较大,则将其更新为
{
dis[v]=dis[u]+w;//更新距离
q.push((node){v,dis[v]});//由于点v被更新,所以当前的v所在路径更短,那么就让点v和对应的距离入队
}
}
}
}
int main()
{
memset(head,0,sizeof(head));
n=read(),m=read(),s=read();
for(int i=1;i<=m;i++)
{
int x,y,z;
x=read(),y=read(),z=read();
add(x,y,z);
}
Dijkstra();
for(int i=1;i<=n;i++)
printf("%d ",dis[i]);
return 0;
}
总结
Dijkstra算法极为重要,需要也应该记忆堆优化后的Dijkstra算法。堆优化后的Dijkstra算法流程大致如下:先预处理出来dis数组,再定义一个优先队列来实现堆,并将源点的信息加入优先队列中,从源点开始通过bfs的方式来处理最短路。处理时,由于我们实现的堆是小根堆,所以说队首元素的路径一定是最小的,那么我们就可以先将队首元素给取出来,并单独处理这个元素,比如我们定义这个队首元素为first,接下来我们再将这个元素的点和最小距离定义出来,方便一会儿的使用。另外我们可以做一个剪枝,由于每次处理完一个点后我们要把它删掉,所以说我们可以特判一下点u的最短路径是否不等于堆中维护的最短路径。接下来就是直接对以该点开始遍历相邻的几个顶点,并看看需不需要更新dis数组所记录的最短路径。bfs部分的简化表述如下:先处理当前点并记录对应信息,再遍历相邻的几个点,并处理相邻几个点的信息,来更新最短路径。
其实Dijkstra算法在理解后是非常容易记忆的。完结撒花(逃