Dijstra 算法
前言
好写也好用。
1. 原理
Dijstra 算法可以求解出从任意一个节点出发到任意可以到达的节点的最短路。也就是单源最短路。
Dijstra 算法同样基于松弛(relax)操作。
具体来说,此算法每次使用已经求得最短路的节点更新其它节点距离原点的距离,每次更新完成后未求得最短路节点集合中距离最小的节点的距离就是它的最段路长度,于是这个节点的最短路被求出,再用这个节点更新其它节点,重复开头的操作操作即可。
从原理中可以看出此算法利用了贪心思想。
关于这样贪心的正确性证明见OIwiki。
注意:Dijstra 算法只能求解无负 权 的图的最短路,不止无负环!注意题目的说明!
有负权边卡 Dijstra 的图:

在上面的图中使用 Dijstra 求解从 \(0\) 节点出发的单源最短路,算法会直接确定节点 \(2\) 的最短路长为 \(3\),但其实最短路是 \(0\rightarrow 1\rightarrow 2\) 的 \(-1\),Dijstra 成功被卡。
Dijstra 算法有两种实现,朴素实现和堆优化实现,下面分开介绍。
2.朴素实现
无论何种实现的 Dijstra 算法均需要准备两个数组:dist 数组和 vis 数组。前者用来存放每个每个节点的最短路长度,后者用来标记此节点的最短路有没有求解出来。
首先将 dist 数组初始化为无限大,vis 数组初始化为 false。
之后将要求解的原节点的 dist 初始化为 \(0\),vis 数组标记为已求出的 true。
使用一个 for 循环遍历找出 dist 数组中值最小的节点编号。
则此时这个节点的最短路一定被求出,将 vis 数组中对应编号的值更新为 true,并使用这个节点的最短路长度更新其它邻接点的 dist 数组。
重复找点的操作,直到所有点的最短路被求出。
示例代码如下:
memset(dist,0x3f,sizeof dist);//所有距离初始化为无限大
memset(vis,0,sizeof vis);//所有节点初始化为未求出
dist[s]=0;//自己到自己没有距离
//这里不要把 vis[s] 标记!!
for(int i=1;i<=n;i++){
int minid;
int ans=0x3f3f3f3f;
for(int i=1;i<=n;i++){//找到所有没有找到最短路的点中距离最短的
if(!vis[i]){
if(ans>dist[i]){
ans=dist[i];
minid=i;
}
}
}
if(minid==-1)break;//如果没有了没有找到最短路的点代表求解完成,直接退出
vis[minid]=1;//先标记,此时 dist[i] 就是从 s 出发的最短路
for(int i=0;i<e[minid].size();i++){//使用这个 minid 点更新其它邻接点
int v=e[minid][i].v,w=e[minid][i].w;
if(dist[minid]+w<dist[v]){//松弛操作
dist[v]=dist[minid]+w;
}
}
}
3.堆优化的 Dijstra
OI 中一般使用插入取出均为 \(O(\log m)\) 的优先队列进行优化,其中 \(m\) 为图中的边数。
容易发现,朴素实现中寻找最小 dist 的操作浪费了时间。
因此我们可以使用一个堆,每次找到这个节点更小的 dist 时就把点编号和 dist 值放入优先队列中,按 dist 值从小到大排。
我们可以发现,从堆顶也就是队头取出的节点一定是所有节点中 dist 值最小的,那么这个点的最短路就被求出了,更新 vis 即可。
因此,插入队列时要判断对方的节点的最短路有没有被求出,如果已经求出就不需要再插入队列了。
同理,在取出队头时也要判断这个点的最短路是否被求出,如果已被求出就不再需要这个点来更新其它点了(因为对于每一个点,后取出来的 dist 值一定比先前取出的大,这并不是这个节点的最短路),直接 continue 即可。
示例代码如下:
struct edge{
int v;
int w;
bool operator<(const edge &x) const{//为了使用自定义排序需要在结构体内部进行运算符重载
return x.w<w;//注意这一句的写法
}
};
priority_queue<edge> q;//准备一个优先队列
...
memset(dist,0x3f,sizeof dist);//所有距离初始化为无限大
memset(vis,0,sizeof vis);//所有节点初始化为未求出
dist[s]=0;//自己到自己没有距离
//这里不要把 vis[s] 标记!!
q.push((edge){s,0});//压入初始节点 s
while(q.size()){
int u=q.top().v,w=q.top().w;//对顶取值,一定是这个点的最短路
q.pop();//一!定!要!弹!出!
if(vis[u])continue;//如果已经找到了就跳过
vis[u]=1;//标记
dist[u]=w;//复制
for(int i=0;i<e[u].size();i++){//遍历所有邻接节点
if(dist[u]+e[u][i].w<dist[e[u][i].v]){//松弛操作
dist[e[u][i].v]=dist[u]+e[u][i].w;//一定要更新不然会浪费大量时间!!!!
q.push((edge){e[u][i].v,dist[u]+e[u][i].w});
}
}
}
还有更多的花式实现例如线段树,树状数组实现等。由于都不如封装好的优先队列好写且常数较大就不列举了。
4. 最短路算法一览表
| 算法名 | Dijstra | Floyd | Spfa | Johnson |
|---|---|---|---|---|
| 最短路类型 | 单源最短路 | 全源最短路 | 单源最短路 | 全源最短路 |
| 适用的图 | 无负权图 | 任意图 | 任意图 | 任意图 |
| 可以判断负环 | 不可以 | 可以 | 可以 | 可以 |
| 时间复杂度 | \(O(m\log m)\) | \(O(n^3)\) | \(O(nm)\) | \(O(nm\log m)\) |
迁移自洛谷

浙公网安备 33010602011771号