Dijkstra算法优化~~你一定可以看懂的四种进阶优化


注:博主已经发表过一篇Dijkstra的算法讲解, 若读者不会此算法的基础版本,还请读者先自行翻阅我的博客,学会Dijkstra算法再看这篇。若有疑问可以私信我或在评论区留言,愿能帮助到你。

Dijkstra算法~~四种进阶优化

一、对边的优化

我们试想一下,在顶点很多的无向图中,边却很少,如果我们还是利用邻接矩阵来实现图的存储的话那么我们会做很多无用功,这样是我们不愿看到的,那么我们如果可以存储边来抽象表示我们的无向图,那岂不是美滋滋,因为我们只需要对较少的边进行运算即可。这里我们会介绍两种方法:1、链式前向星存储;2、vector实现邻接表

使用这种优化的前提就是:实际边远小于无向图的最大边数

1、链式前向星

在说链式前向星之前我们先了解前向星,前向星是一种数据结构,以存储边的方式来存储图。构造方法如下:读入每条边的信息,将边存放在数组中,把数组中的边按照起点顺序排序,前向星就构造完了。通常用在点的数目太多,或两点之间有多条弧的时候。一般在别的数据结构不能使用的时候才考虑用前向星。除了不能直接用起点终点定位以外,前向星几乎是完美的。

struct edge{
    int start;//边的起点
    int to;//边的终点
    int w;//边的权值。
}q[10001];
//起点就是在边的序号,所以我们读入之后要按起点顺序进行排序。

这种方法有种弊端,就是边与边的关系没有联立起来,尤其在我们的Dijkstra算法中就是根据同一点的边进行拓展的,所以在这基础上我们推出了链式前向星,这种就可以根据起点来进行组合,形成跟邻接表一样的效果,但比邻接表要简单的多。我们来看下这是怎么联立的,代码附了详细解释。

typedef struct Edge{
	int next;//边起点相同的第一条边的编号。
	int to; //边的终点顶点编号。
	int w;//边权值
}Edge;
Edge edge[M];//M为边的最大值。
int head[maxn];//maxn为顶点的最大值。对于head[i]来说:存储以i为起点的第一条边的编号。记住是第一条边。我们一般都要初始化head数组为-1,即不存在第一条边。

这个核心就是利用head数组和边集合中的next来联立的,第一条边的上一条边永远是-1,这在之后整个的代码实现中会看到。我们存储好边之后,就可以开始我们的关键Dijkstra算法的实现了,还是以贪心的思想去实现,我们来看代码。

#include<iostream>
#include<memory.h>
#include<algorithm>
#include<cstdio>

using namespace std;

const int maxn=105;//最大顶点数。
const int M=1e4;//最大边数。
int n,m;//实际顶点数和边数。
const int inf=0x3f3f3f3f;//表示无穷大。
int dis[maxn];//存储源点到其余的最短距离。
bool visited[maxn];//判断是否已经确定最短距离
typedef struct Edge{
	int next;//边起点相同的下一条边的编号。
	int to; //边的终点顶点编号。
	int w;//边权值
}Edge;
Edge edge[M];
int head[maxn];//存储以i为起点的第一条边的编号。
int tot;//递增编号。
void init(){
	memset(visited,false,sizeof(visited));
	memset(dis,inf,sizeof(dis));
	memset(head,-1,sizeof(head));
	tot=0;//编号。
}
void add(int u,int v,int w){
	edge[tot].to=v;//记录终点
	edge[tot].w=w; //记录权值。
	edge[tot].next=head[u];//插入到第一条边前面。
	head[u]=tot++;//更改第一条边的编号。
}
void dijkstra(){
	int minn,pos;//minn记录最小值,pos记录最小值的下标	
    //由于我们是用边来存储,那么就不能使用for来进行加点操作了,那么我们可以设立一个死循环,当所有点都已确定最短距离后,即可发挥作用了。
	while(1){
		minn=inf,pos=-1;
		for(int i=1;i<=n;i++){
			//遍历找出当前的最短的最短路径。
			if(!visited[i]&&dis[i]<minn){
				minn=dis[i];
				pos=i;
			}
		}
		if(pos==-1){
			//说明所有顶点都已经被确定了,此时我们这个算法就可以结束了。
			break;
		}
		visited[pos]=true;
		for(int i=head[pos];i!=-1;i=edge[i].next){ //这里要认真理解,i是边的编号,当i=-1时,即i到了最后一条边的下一条边,显然不存在。
			int t=edge[i].to;//记录每条边的终点。
			if(!visited[t]&&dis[t]>dis[pos]+edge[i].w){  //更新最短路径。
				dis[t]=dis[pos]+edge[i].w;
			}
		}
	}
	return;
}
int main()
{
	while(cin>>n>>m){
		init();
		int u,v,w;
		for(int i=0;i<m;i++){
			cin>>u>>v>>w;//如果m太大了,一定要使用scanf输入,不然可能会超时。
			add(u,v,w);    //加点,若是有向图,我们则只进行add(u,v,w);
			add(v,u,w);
		}
		int S,E;
		cin>>S>>E;//起点和终点
		dis[S]=0; //将起点的dis置为0,实现Dijkstra算法的时候S就为源点,也就是将起点的最短路径置为0,其余都为inf,这样就可以使得dis数组是其余顶点到源点的最短距离了。
		dijkstra();
		cout<<dis[E]<<endl;
	}
	return 0;
}

2、vector实现邻接表

这种和邻接表一样的思想,但实现起来比链式存储的要简单的多,相当于是N个边数组。我们来看存储结构的实现

typedef struct Edge{
	int to; //边的终点顶点编号。
	int w;//边权值
}Edge;
vector<Edge> graph[maxn];//利用vector表示我邻接表
const int maxn=105;//最大顶点数。

没错,你没看错,就是这么简单,这graph[i]对应的就是以起点为i的边向量,那么若起点i没有边,则该向量自然为空,这比链式前向星要更好理解。下面看整个的实现吧,核心几乎没变,因为都是对边进行操作,如果你领悟了上一种,这种也自然可以解决了。

#include<iostream>
#include<memory.h>
#include<algorithm>
#include<cstdio>
#include<vector>

using namespace std;

const int maxn=105;//最大顶点数。
const int M=1e4;//最大边数。
int n,m;//实际顶点数和边数。
const int inf=0x3f3f3f3f;//表示无穷大。
int dis[maxn];//存储源点到其余的最短距离。
bool visited[maxn];//判断是否已经确定最短距离
typedef struct Edge{
	int to; //边的终点顶点编号。
	int w;//边权值
}Edge;
void init(){
	memset(dis,inf,sizeof(dis));
	memset(visited,false,sizeof(visited));
}
vector<Edge> graph[maxn];//利用vector表示我邻接表

void dijkstra(){
	int minn,pos;//minn记录最小值,pos记录最小值的下标	//由于我们是用边来存储,那么就不能使用for来进行加点操作了,那么我们可以设立一个死循环,当所有点都已确定最短距离后,即可发挥作用了。
	while(1){
		minn=inf,pos=-1;
		for(int i=1;i<=n;i++){
			//遍历找出当前的最短的最短路径。
			if(!visited[i]&&dis[i]<minn){
				minn=dis[i];
				pos=i;
			}
		}
		if(pos==-1){
			//说明所有顶点都已经被确定了,此时我们这个算法就可以结束了。
			break;
		}
		visited[pos]=true;
		int v,len;//存储以pos为起点的边的终点和权值。
		for(int i=0;i<graph[pos].size();i++){
			//遍历pos顶点所有的边,更新最短路径。
			v=graph[pos][i].to;len=graph[pos][i].w;
			if(!visited[v]&&dis[v]>dis[pos]+len)
				dis[v]=dis[pos]+len;
		}
	}
	return;
}
int main()
{
	while(cin>>n>>m){
		init();
		int u,v,w;
		Edge temp1,temp2;
		for(int i=0;i<m;i++){
			cin>>u>>v>>w;//如果m太大了,一定要使用scanf输入,不然可能会超时。
			temp1.to=v;temp2.to=u;
			temp1.w=temp2.w=w;
			graph[u].push_back(temp1);
			graph[v].push_back(temp2);//加边,同样如果是有向图只加一条边,且方向要对。
		}
		int S,E;
		cin>>S>>E;//起点和终点
		dis[S]=0; //将起点的dis置为0,实现Dijkstra算法的时候S就为源点,也就是将起点的最短路径置为0,其余都为inf,这样就可以使得dis数组是其余顶点到源点的最短距离了。
		dijkstra();
		cout<<dis[E]<<endl;
	}
	return 0;
}

二、利用优先队列实现对时间的优化

我们发现在Dijkstra算法中,总是要寻找当前的最短的最短路径,这是需要时间来查找的,但如果我们利用优先队列的自动排序的功能,即用堆实现这个功能,在每一趟中选择队头元素,这就是我们想要的最短路径的相关信息,也就是这样我们就可以不用去查找了,我们接下来介绍我们的这两种优化:1、链式前向星优化。2、vector邻接表优化。是基于第五种来的,也是最终版本。

非常重要的一点就是,我们把所有更新好了的最短路径全部加入队列中,这是没问题的,因为如果有个点已经确定了最短路径,那么这条语句就会发挥作用:if(visited[v])continue;每次取出来的都是目前最短的最短路径信息。同样,也是因为这条语句,当所有点确定完之后自然会导致队空。只要了解了优先队列,了解了第五种优化,那么这种超级优化读者应该可以看懂的

PS:对于pair类型,优先队列会先比较第一个的值,若相等,再判断第二个的值。

1、链式前向星优化

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<algorithm>
#include<cmath>
#include<string>
#include<stack>
#include<queue>
#include<cstring>
#include<map>
#include<iterator>
#include<list>
#include<set>
#include<functional>
#include<memory.h>

using namespace std;

const int maxn=105;//顶点最大数
const int M=1e4;//边数最大值
const int inf=0x3f3f3f3f;//无穷大
typedef struct Edge{
	int next;//起点相同的下一条边的序号。
	int to;//该边的终点。
	int w;//该边的权值。
}Edge;
Edge edge[M];//边数组。
int head[maxn];//联系边之间的关系数组,head[i]表示起点为i的第一条边的序号
bool visited[maxn];//若为true,则代表已经确定最短路径
typedef pair<int,int> pll;//first代表最短路径,second代表该最短路径的起点。
int dis[maxn];//存储最短路径。
void init(){
	memset(visited,false,sizeof(visited));
	memset(dis,inf,sizeof(dis));
	memset(head,-1,sizeof(head));//用-1表示第一条边不存在,即该顶点没边。
}
int n,m;//实际顶点数和边数。
int cnt;//递增序号,为边赋值。
void add(int u,int v,int w){
	//加边操作。
	edge[cnt].to=v;
	edge[cnt].w=w;
	edge[cnt].next=head[u];
	head[u]=cnt++;
}
void dijkstra(int S){
	priority_queue<pll,vector<pll>,greater<pll> > q;//优先队列。
	dis[S]=0;
	q.push(pll(0,S));//左边最短路径,右边顶点序号
	pll temp;
	cnt=0;
	while(!q.empty()){
		temp=q.top();
		q.pop();
		int u=temp.second;
		if(visited[u])continue;//已经确定了就跳过
		visited[u]=true;
		int t,len;
		for(int i=head[u];i!=-1;i=edge[i].next){
			t=edge[i].to;len=edge[i].w;//记录边终点和权值。
			if(!visited[t]&&dis[t]>dis[u]+len){
				dis[t]=dis[u]+len;
				q.push(pll(dis[t],t));
			}
		}
	}
}
int main(){
	while(cin>>n>>m){
		int u,v,w;
		init();
		for(int i=0;i<m;i++){
			cin>>u>>v>>w;//如果m太大了,一定要使用scanf输入,不然可能会超时。
			add(u,v,w);
			add(v,u,w);
		}
		int S,E;//起点和终点。
		cin>>S>>E;
		dijkstra(S);
		cout<<dis[E]<<endl;
	}
	return 0;
}

2、vector邻接表优化

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<algorithm>
#include<cmath>
#include<string>
#include<stack>
#include<queue>
#include<cstring>
#include<map>
#include<iterator>
#include<list>
#include<set>
#include<functional>
#include<memory.h>

using namespace std;

typedef pair<int,int> Pair;//first代表权值,second代表顶点序号。
const int maxn=105;//最大顶点数。
const int M=1e4;//最大边数。
int n,m;//实际顶点数和边数。
const int inf=0x3f3f3f3f;//表示无穷大。
int dis[maxn];//存储源点到其余的最短距离。
bool visited[maxn];//判断是否已经确定最短距离
typedef struct Edge{
	int to; //边的终点顶点编号。
	int w;//边权值
	//对于自定义类型的优先队列,我们需要些仿函数进行运算符重载。
}Edge;
void init(){
	memset(dis,inf,sizeof(dis));
	memset(visited,false,sizeof(visited));
}
vector<Edge> graph[maxn];//利用vector表示我邻接表

void dijkstra(int S){
    priority_queue<Pair,vector<Pair>,greater<Pair> > q;//优先队列,小顶堆。
    q.push(Pair(0,S));//将起点入队。
    while(!q.empty()){
		Pair temp=q.top();//注意这里是要用top函数,在优先队列中没有front函数。
		q.pop();//出队。
		int v=temp.second;//取该最短路径的边的终点序号。
		if(visited[v])continue;//如果已经确定了就不能往下更新。为了避免重复更新。
		visited[v]=true;
		//以v为点开始遍历它的边,
		Edge e;//临时变量,存储点和边。
		for(int i=0;i<graph[v].size();i++){
			//遍历
			e=graph[v][i];
			if(!visited[e.to]&&dis[e.to]>dis[v]+e.w){
				dis[e.to]=dis[v]+e.w;
				q.push(Pair(dis[e.to],e.to));//入队。
			}
		}
    }
	return;
}
int main()
{
	while(cin>>n>>m){
		init();
		int u,v,w;
		Edge temp1,temp2;
		for(int i=0;i<m;i++){
			cin>>u>>v>>w;//如果m太大了,一定要使用scanf输入,不然可能会超时。
			temp1.to=v;temp2.to=u;
			temp1.w=temp2.w=w;
			graph[u].push_back(temp1);
			graph[v].push_back(temp2);//加边,同样如果是有向图只加一条边,且方向要对。
		}
		int S,E;
		cin>>S>>E;//起点和终点
		dis[S]=0; //将起点的dis置为0,实现Dijkstra算法的时候S就为源点,也就是将起点的最短路径置为0,其余都为inf,这样就可以使得dis数组是其余顶点到源点的最短距离了。
		dijkstra(S);
		cout<<dis[E]<<endl;
	}
	return 0;
}

猜你喜欢

转载自blog.csdn.net/hzf0701/article/details/107674290