Dijkstra算法学习笔记
updated. 2019.11.3
Dijkstra是用于求解正权图上的单源最短路径(SSSP)问题的算法
在这里我也想说一下关于SPFA和Dij Dij是一个求没有负权边的图的单源最短路的算法 SPFA是一个求存在负权边的图的单源最短路以及判负环的算法 看似一字之差 写起来也很像 但其实Dij和SPFA完全是不同的思想 Dij是贪心思想 利用了一个只有没有负权边的图才拥有的性质 而SPFA更像是一个"暴力"算法
另外 SPFA没死
下面是正文
算法流程
Dijkstra算法其实是一个基于贪心的过程
正确性会在后面提及
设起点是\(s\)
\(dis_u\)表示目前从\(s\)到\(u\)的最短路径
\(vis_u\)表示\(u\)点的最短路是否已经被确定
\(1.\)初始化起点的距离\(dis_s\)为0 其它节点都是正无穷
\(2.\)找到所有未确定最短路的点中 与起点的距离\(dis_u\)最小的点\(u\)
\(3.dis_u\)一定是起点到\(u\)最短路的长度了 \(u\)的最短路变为已确定
\(4.\)扫描\(u\)的所有出边 如果\(s\to u\to v\)这条路径更优(即\(dis_u+w(u,v)<dis_v\)) 就把\(dis_v\)更新为\(dis_u+w(u,v)\)
\(5.\)重复2~4直到所有点的最短路都被确定
这里放张图理解
代码在后面
之前画的图太丑了 所以自己手画了一幅
蓝点表示已经确定了最短路 打\(\sqrt{}\)的代表按距离的小根堆的堆顶
最后放一张动图
动图炸了 所以点这里吧/kk
正确性
Dijkstra是基于贪心思想实现的
在每一轮更新后 还未访问到过的节点一定不优于已经访问到过的节点
而在已经访问到过的节点中 找出未确定最短路且当前\(dis_u\)最小的\(u\)
由于其它点的\(dis\)本来就已经比\(dis_u\)大 边权还是非负数 它们不可能更新\(dis_u\) 所以\(dis_u\)已经被确定了 \(u\)的最短路就确定了 这样一步一步下来可以确定所有点的最短路
优化
寻找全局最小值这一步是可以优化的
加入元素、查找最小值、删除最小值
用堆来实现这些操作非常合适
所以就有了堆优化的Dijkstra
堆中每个元素存两个值 结点编号\(x\)和入堆是该点被更新成的距离\(dis\)
\(dis\)为第一关键字(即按dis的小根堆)
主要思想不变 每次找距离最小点是直接取出堆顶并删除堆顶
更新最短路的时候 将更新后的节点入堆
注意一个点可能会被更新多次而入堆多次 但是只有最后一次入堆才是正确的dis 同时也一定是在所有入堆操作结束后才会出堆 所以直接开个数组判断有没有出堆过就好了 当然也可以根据堆中元素的距离大小和点的最短路大小直接判断
直接给出堆优化Dijkstra代码:
struct node{
LL dis,x;
bool operator < (const node &nd)const{ return nd.dis < dis; }
// 重载运算符 注意pq的重载是反的
}h;
priority_queue <node> q; // 这叫堆
void add_edge(int f,int t,int C){
++ ec; to[ec] = t; cst[ec] = C; nxt[ec] = hed[f]; hed[f] = ec;
}
void dijkstra(){
memset(dis,0x3f,sizeof(dis));
memset(vis,0,sizeof(vis));
dis[s] = 0; h.x = s; h.dis = 0; q.push(h);
while(!q.empty()){
h = q.top(); q.pop();
int u = h.x,v;
if(vis[u]) continue; vis[u] = 1;
for(int i = hed[u];i;i = nxt[i]){
v = to[i];
if(dis[v] > dis[u] + cst[i]){
dis[v] = dis[u] + cst[i];
if(!vis[v]){
h.x = v; h.dis = dis[v];
q.push(h);
}
}
}
}
for(int i = 1;i <= n;i ++) printf("%d ",dis[i]);
}
int main(){
scanf("%d %d %d",&n,&m,&s);
while(m --){
scanf("%d %d %d",&a,&b,&c);
add_edge(a,b,c);
}
dijkstra();
return 0;
}
时间复杂度证明:
由于每一个点只会出堆一次(严格来说是只扩展这个节点一次)
所以每一个点和每一条边都只会访问一次
堆优化Dijkstra的时间复杂度为\(O((N+M)logM)\)
另外 如果图中存在负权边 即使没有负环 Dijkstra 也会有错误
如果不用新数组判断而用距离\(dis\)来判断一个点是否入堆过 那么会被卡成指数级复杂度
简单应用
在求最短路的过程中可以同时维护一些其他信息
这些信息也应该能够传递
1.记录路径
每次更新最短路的时候记一个pre即可 代表这条路径是由哪个点过来的
最后直接从最后一个点不断取pre 输出路径
void dijkstra(){
memset(dis,0x3f,sizeof(dis));
memset(vis,0,sizeof(vis));
dis[s] = 0; t.x = s; t.dis = 0; q.push(t);
while(!q.empty()){
h = q.top(); q.pop();
int u = h.x,v;
if(vis[u]) continue; vis[u] = 1;
for(int i = hed[u];i;i = nxt[i]){
v = to[i];
if(dis[v] > dis[u] + cst[i]){
dis[v] = dis[u] + cst[i];
pre[v] = u;
t.x = v; t.dis = dis[v];
q.push(t);
}
}
}
}
while(now){
printf("%d ",now);
now = pre[now];
}
2.最短路计数
记录一个\(cnt_u\)代表起点到u的最短路有多少条
在\(dis_v=dis_u+w(u,v)\)时 加上来自\(u\)的方案数
void dijkstra(int s){
memset(dis,0x3f,sizeof(dis));
memset(vis,0,sizeof(vis));
memset(cnt,0,sizeof(cnt));
dis[s] = 0; cnt[s] = 1; t.x = s; t.dis = 0; q.push(t);
while(!q.empty()){
h = q.top(); q.pop();
int u = h.x; if(vis[u]) continue; vis[u] = 1;
for(int i = hed[u];i;i = nxt[i]){
int v = to[i];
if(dis[v] > dis[u] + w[i]){
dis[v] = dis[u] + w[i]; cnt[v] = cnt[u];
t.x = v; t.dis = dis[v];
q.push(t);
}
else if(dis[v] == dis[u] + w[i]){
cnt[v] += cnt[u];
cnt[v] %= N;
}
}
}
}
3.次短路
记录到每个点的最短路和次短路
这里由于边能够重复走 就不能够使用是否出过堆判断 一个点可能要更新其它点多次
这里只要用路程判断 就是堆中元素\(\{x,dis\}\)的\(dis\)比次短路\(dis2_x\)还大 就抛弃这个元素 取下一个
还有一种神奇的解法 把每一条边再加一条边 起点终点相同 边权为原来的三倍 代表\(u\to v\to u\to v\)
void dijkstra(int fr){
memset(dis,0x3f,sizeof(dis)); dis[fr] = 0;
memset(ds2,0x3f,sizeof(ds2));
memset(vis,0,sizeof(vis));
priority_queue <node> q;
t.x = fr; t.dis = 0; q.push(t);
while(!q.empty()){
h = q.top(); q.pop();
int u = h.x; if(vis[u]) continue; vis[u] = 1;
for(int i = hed[u];i;i = nxt[i]){
int v = to[i];
if(dis[v] > dis[u] + w[i]){
ds2[v] = dis[v];
dis[v] = dis[u] + w[i];
t.x = v; t.dis = dis[v];
q.push(t);
}
else if(ds2[v] > dis[u] + w[i] && dis[v] != dis[u] + w[i]) ds2[v] = dis[u] + w[i];
if(ds2[v] > ds2[u] + w[i]) ds2[v] = ds2[u] + w[i];
}
}
}
for(int i = 1;i <= m;i ++){
scanf("%d %d %d",&a,&b,&c);
add_edge(a,b,c);
add_edge(b,a,c);
add_edge(a,b,3 * c);
add_edge(b,a,3 * c);
}