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);
}
posted @ 2020-04-21 13:38  ItzInstallB  阅读(157)  评论(0编辑  收藏  举报