【C++】Dijkstra算法

1,125 阅读2分钟

「这是我参与11月更文挑战的第10天,活动详情查看:2021最后一次更文挑战」。

算法简介

迪杰斯特拉(Dijkstra)算法是图论中的最短路算法,它可以实现求解特定起点到任一点的最短路径。对于顶点个数为nn的图,如果需要求解每两个点之间的最短路径则需要跑nn次迪杰斯特拉算法。

迪杰斯特拉的时间复杂度为O(n2)O(n^2),缺点是不适用于带负权边的图。

算法思想

迪杰斯特拉算法基于贪心的思想,不断地寻找中间结点ww使得uwvu\to w\to v的距离小于uvu\to v的距离,即用中间值更新结果。相似算法思想参考朴素迪杰斯特拉算法邻接矩阵法。

朴素迪杰斯特拉算法

邻接矩阵法

这是最简单的迪杰斯特拉算法,采用邻接矩阵存储,这里以邻接矩阵map[i][j]map[i][j]表示iijj的距离,顶点到自身距离map[i][i]=0map[i][i]=0,如果iijj没有边则map[i][j]=map[i][j]=\infin

对于一个顶点个数为nn的图,若求点xx到任一点的最短路径长度,迪杰斯特拉维护一个长度为nn的数组dist[]dist[]dist[y]dist[y]表示xxyy的最短路径长度。其次,还维护了一个长度为nnvisit[]visit[]数组,visit[j]visit[j]表示jj是否被访问过。

  1. 对于distdist数组,首先初始化dist[]dist[]\infin表示xx到任一点距离无穷大,但是dist[x]=0dist[x]=0表示xx到自己距离为00,初始化visit[]visit[]数组值为00表示点未被访问,执行第22步。
  2. 查找未被访问过的顶点中使得dist[j]dist[j]最小的顶点jj,并把jj标记为已访问,即visit[j]=1visit[j]=1,执行第33步。
  3. jj去更新路径,查看以jj作为中间结点是否比已知路更短,更短则更新。dist[y]=min(dist[y],dist[j]+map[j][y])dist[y]=min(dist[y],dist[j]+map[j][y]),回到第22步,直到所有顶点都被访问。
void Dijkstra(int x){//求x到任一点
	memset(dist,INF,sizeof(dist));//dist[]=INF
	dist[x]=0;
	memset(visit,0,sizeof(visit));
	for(int i=1;i<=n;i++){
		int j=0;
		for(int k=1;k<=n;k++){//找最小未被访问的dist[j]
			if(!visit[k]&&dist[k]<=dist[j]){
				j=k;
			}
		}
		visit[j]=1;
		for(int k=1;k<=n;k++){//更新dist[j]
			dist[k]=min(dist[k],dist[j]+map[j][k]);
		}
	}
}

邻接表法

除了易于实现的邻接矩阵,迪杰斯特拉算法同样可以改写成邻接表形式。这里采取链式前向星结构,其实所谓的链式前向星,就是用数组模拟实现的链表,也就是静态链表。head[]head[]数组表示顶点数组,指向与顶点连接的边的下标。to[k]to[k]数组表示第kk条边的指向顶点编号,val[k]val[k]表示第kk条边的权值。

void Dijkstra(int x){//求x到任一点
	memset(dist,INF,sizeof(dist));//dist[]=INF
	dist[x]=0;
	memset(visit,0,sizeof(visit));
	for(int i=1;i<=n;i++){
		int j=0;
		for(int k=1;k<=n;k++){//找最小未被访问的dist[j]
			if(!visit[k]&&dist[k]<=dist[j]){
				j=k;
			}
		}
		visit[j]=1;
		for(int k=head[j];k;k=next[k]){//更新dist[j]
			dist[to[k]]=min(dist[to[k]],dist[j]+val[k]);
		}
	}
}

堆优化迪杰斯特拉算法

  • 朴素迪杰斯特拉算法的可优化点在于查找未被访问过的满足最小dist[j]dist[j]的顶点jj,这个过程可以采取堆优化,也就是所谓的优先队列。

算法思想

优化查找过程,首先初始化堆只有源点,初始化dist[]=INFdist[]=INF,除了dist[x]=0dist[x]=0。然后每次从堆中取出最小距离的中间结点jj并出堆,将所有能更新的dist[j]+map[j][k]<dist[k]dist[j]+map[j][k]<dist[k]dist[k]dist[k]更新为dist[j]+map[j][k]dist[j]+map[j][k]并将结点kk入堆,重复此过程直到堆空为止。

提示

  1. 由于朴素算法是用数组编号来映射结点的,而堆排序会打乱原映射顺序,所以我们考虑用C++C++自带的pair<typeA,typeB>pair<typeA,typeB>来存储结点,由于pairpair默认按照typeAtypeA排序,所以typeAtypeA应该是距离,typeBtypeB是结点编号。
  2. c++c++priority_queuepriority\_queue默认是大顶堆,所以需要重载比较运算符。
  3. priority_queuepriority\_queue位于<queue><queue>pairpair位于<utility><utility>

邻接矩阵法

void Dijkstra(int x){//求x到任一点
	memset(dist,INF,sizeof(dist));//初始化最远距离
	typedef pair<int,int> pr;//分别存储距离和顶点编号
	priority_queue<pr,vector<pr>,greater<pr> > Q;//优先队列,重载运算符
	dist[x]=0;//初始化源点自身距离为0
	Q.push(make_pair(0,x));
	while(!Q.empty()){
		int j=Q.top().second;
		Q.pop();
		if(visit[j]) continue;
		visit[j]=1;
		for(int k=1;k<=n;k++){//更新dist[j]
			if(dist[j]+map[j][k]<dist[k]){
				dist[k]=dist[j]+map[j][k];
				if(!visit[k]) Q.push(make_pair(dist[k],k));
			}
		}
	}
}

邻接表法

同样的,堆优化也可以采用链式前向星结构,详细数组含义参考朴素算法的邻接表法。

void Dijkstra(int x){//求x到任一点
	memset(dist,INF,sizeof(dist));//初始化最远距离
	typedef pair<int,int> pr;//分别存储距离和顶点编号
	priority_queue<pr,vector<pr>,greater<pr> > Q;//优先队列,重载运算符
	dist[x]=0;//初始化源点自身距离为0
	Q.push(make_pair(0,x));
	while(!Q.empty()){
		int j=Q.top().second;
		Q.pop();
		if(visit[j]) continue;
		visit[j]=1;
		for(int k=head[j];k;k=last[k]){//更新dist[j]
			if(dist[j]+val[k]<dist[to[k]]){
				dist[to[k]]=dist[j]+val[k];
				if(!visit[to[k]) Q.push(make_pair(dist[to[k]],to[k]));
			}
		}
	}
}

总结

迪杰斯特拉算法堆优化后效率很高,优于SPFASPFA算法,但是不能用于带负权边的图,时间复杂度上堆优化效率高于朴素算法,空间复杂度上邻接表法优于邻接矩阵法。