Dijkstra算法基础

目录

概念

核心步骤

“松弛”操作

存图的方法

链式前向星

题目讲解-方法一

头文件及变量定义

主函数部分

输入、存图

初始化

Dijkstra算法主体

输出

完整代码

题目讲解-方法二

总结


本蒟蒻今天刚学Dijkstra算法,写一篇文章理理思路。

概念

Dijkstra算法用于求单源最短路(给定一个起点,输出其到多个结点的最短路径)。

像这种带边权的题目,用BFS是不行的,因为BFS搜的是路径最短,如果遇到如下图的情况,那么就会输出错误的结果。

如图,求的是结点A到结点B的最短路径,如果我们用BFS,则它第一次就扩展到了B点,直接选择了边权为10的路径,那么就不对了,还有一条更短的路明摆着呢!

这时候可能有人会说:“这不合常理啊,边长都一样,路也应该一样长啊!”确实,如果我们按照正常思维来讲,当然是不对的,小学生都学过“三角形的两边之和大于第三边”嘛(我可以骄傲地这样说,是因为我马上就是个初中生了😜)这里引用一下我编程老师的话:“其实我不赞成把边权叫做‘长度’,我觉得应该叫做‘花费’。”所以,如果将边权叫做“花费”,那么就是可以成立的。就类似于这条路很“火”,想在这条路上“游玩”要花的钱就会比较多。

假设有负权边,那么就会出现问题。当存在负权边时,将新节点加入最短路集合的贪心
操作不成立,从而出现错误。这时候就要用到SPFA算法。(还没学,请多指教)

核心步骤

Dijkstra算法的核心思路是如下:

1.在所有未经过向外扩展的结点中找到dis最小点x。

2.对于每条从x出发的边(x,y),对终点y进行“松弛”操作。

“松弛”操作

设dis[v]为起点s到结点v的最短路,对于一条边起点u,终点v,路径长度为w。若dis[u]+w<dis[v],则更新dis[v]=dis[u]+w。操作原理如下图。

如图,可以看出原本直接从s到v的路径较长,然而使用“中转站”会更短。于是将dis[v]更新为设立“中转站”的距离(1+2=3),这本质上是一种贪心的操作。

存图的方法

我将讲解的代码有两个“版本”,一是“原版”,不加任何优化,时间复杂度约为O(n²),为普及组的难度;二是优化版,运用优先队列优化,时间复杂度约为O(mlogn),是提高组的难度。

链式前向星

要存图,最方便的三种方法莫过于邻接矩阵、vector和链式前向星了。下面分析三种方法的利弊。

1.邻接矩阵方法:若由结点u到结点v有一条权重为w的边,那么记edge[u][v]=w。特别地,在无向图中,若结点u与结点v之间有边,则edge[u][v]和edge[v][u]都需要记录,因为双向都有边。

邻接矩阵在遇到稀疏图(边的数目远远小于点的数目的平方的图)时,会造成很多空间的浪费,比较适合存储结点数较少的稠密图。

.

如上图,第一个为无向图,第二个为有向图,可见无向图的邻接矩阵是对称的。

(本图非原创,感谢本图作者,源自百度中搜的图片,原图链接

2.vector存图:和邻接矩阵类似,只是“随开随用”,不会浪费太多空间。大致思路就是开辟一个一维的vector数组(一维的动态数组等于二维)或套两层动态数组(较麻烦)。例如结点u到结点v有一条边,则执行edge[u].push_back(v)。和邻接矩阵一样,vector在遇到无向图时也需双向建边。

重点来了:vector的缺点:只要边太多,数据稍微比空间大一点点,但是根据vector的内部逻辑,它都会自动给你开双倍空间,导致MLE。各位慎用!

 

3.链式前向星:据说是由一名山东的老师研究。使用链表的方式,用“头插法”存储每条边。几乎适用于所有场合。这里多来个代码图片哈,链式前向星的一个基础程序如下,代码实现了输入一个有向图,按顺序输出所有边的起点与终点。具体过程可以参考注释,我后面也许会写一篇讲解链式前向星的文章。

#include<bits/stdc++.h>
const int MN=200005;
int n,m,to[MN],head[MN],nxt[MN];
//head[i]:以i点作为起点的最新一条边的编号
//nxt[i]:同起点的i的下一条边的编号
//to[i]:i号边的终点
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++){
		int x,y;
		scanf("%d%d",&x,&y);
		//注意顺序,先连接再改变
		nxt[i]=head[x];//创建链表,使最新的边指向同一个起点的边[如果有同一个起点的边,那么插入到前面]
		head[x]=i;//以x号起点作为起点的最新一条边为tot号边
//		head[x]=i;
		to[i]=y;
	}
	for(int i=1;i<=n;i++){
		printf("%d:",i);//以i作为起点
		for(int j=head[i];j;j=nxt[j])
			printf("%d ",to[j]);
		printf("\n");
	}
	return 0;
}

题目讲解-方法一

头文件及变量定义
#include<bits/stdc++.h>
#define M 500005
int n,m,u,v,w,s,head[10005],to[M],nxt[M],val[M],dis[10005];
bool vis[10005];

定义执行必需的链式前向星数组和dis数组,并用vis数组记录每个店是否“松弛”过。

主函数部分

输入、存图

一个标准的链式前向星存图。

scanf("%d%d%d",&n,&m,&s);
for(int i=1;i<=m;i++){//输入m条边 
	scanf("%d%d%d",&u,&v,&w);
	to[i]=v;//当前i号边尾节点 
	val[i]=w;//边权 
    nxt[i]=head[u];//头插法 插向同一结点的最新一条边 
	head[u]=i;//当前i号边记录为最新一条边 
}
初始化

鉴于题目数据数据范围:,我们不能将dis数组初始化为0x3f3f3f3f了,因为还不够大。这里介绍一个简单的方法,不用手打“2147483647”,直接上INT_MAX。

for(int i=0;i<=n;i++)//初始化,题目数据太大,不能赋值为0x3f 
		dis[i]=INT_MAX;
	dis[s]=0;//起点距离自己为0
Dijkstra算法主体

不断取距离的结点进行松弛操作,更新最短距离并标记松弛结束。

while(1){
	u=0;//以一个不存在的结点最为最大值
	for(int i=1;i<=n;i++)
		if(dis[i]<dis[u]&&!vis[i])//没有被扩展过并且最小
			u=i; 
	if(!u)//全部结点都松弛过了,退出 
		break;
	//选出了当前最小距离,以u号结点往外松弛 
	for(int i=head[u];i;i=nxt[i]){
		v=to[i],w=val[i];
		if(dis[v]>dis[u]+w)//更新最短距离 
			dis[v]=dis[u]+w;
	}
	vis[u]=1;//u号结点松弛结束(所有与它相邻的点) 
} 
输出

单源最短路就是输出所有结点距离起点的最短距离,这里输出就不用说了吧。

for(int i=1;i<=n;i++)//输出距离起点的距离 
	printf("%d ",dis[i]);
完整代码
#include<bits/stdc++.h>
#define M 500005
int n,m,u,v,w,s,head[10005],to[M],nxt[M],val[M],dis[10005];
bool vis[10005];
int main(){
	scanf("%d%d%d",&n,&m,&s);
	for(int i=1;i<=m;i++){//输入m条边 
		scanf("%d%d%d",&u,&v,&w);
		to[i]=v;//当前i号边尾节点 
		val[i]=w;//边权 
		nxt[i]=head[u];//头插法 插向同一结点的最新一条边 
		head[u]=i;//当前i号边记录为最新一条边 
	}
	for(int i=0;i<=n;i++)//初始化,题目数据太大,不能赋值为0x3f 
		dis[i]=INT_MAX;
	dis[s]=0;//起点距离自己为0
	while(1){
		u=0;//以一个不存在的结点最为最大值
		for(int i=1;i<=n;i++)
			if(dis[i]<dis[u]&&!vis[i])//没有被扩展过并且最小
				u=i; 
		if(!u)//全部结点都松弛过了,退出 
			break;
		//选出了当前最小距离,以u号结点往外松弛 
		for(int i=head[u];i;i=nxt[i]){
			v=to[i],w=val[i];
			if(dis[v]>dis[u]+w)//更新最短距离 
				dis[v]=dis[u]+w;
		}
		vis[u]=1;//u号结点松弛结束(所有与它相邻的点) 
	} 
	for(int i=1;i<=n;i++)//输出距离起点的距离 
		printf("%d ",dis[i]);
	return 0;
} 

题目讲解-方法二

这里我们发现,复杂度主要是由枚举距离最小的结点产生的,因此我们可以在这里用优先队列进行优化,每次压入一个顶点就内部自动排序。代码比较麻烦,需要重载小于运算符。主要改的部分是需要加上结构体,不再需要枚举。代码如下,具体可以参考注释。

#include<bits/stdc++.h>
#define M 500005
using namespace std;
int n,m,u,v,w,s,head[10005],to[M],nxt[M],val[M],dis[10005];
bool vis[10005];
struct node{
	int id,dis;//号数,距离 
	bool operator < (const node &a) const{//重载运算符 
		return dis>a.dis;
	}
}; 
priority_queue<node>q;//定义优先队列
int main(){
	scanf("%d%d%d",&n,&m,&s);
	for(int i=1;i<=m;i++){//输入m条边 
		scanf("%d%d%d",&u,&v,&w);
		to[i]=v;//当前i号边尾节点 
		val[i]=w;//边权 
		nxt[i]=head[u];//头插法 插向同一结点的最新一条边 
		head[u]=i;//当前i号边记录为最新一条边 
	}
	for(int i=0;i<=n;i++)//初始化,题目数据太大,不能赋值为0x3f 
		dis[i]=INT_MAX;
	q.push(node{s,0});//压入一个点 
	dis[s]=0;//起点距离自己为0
	while(!q.empty()){
		u=q.top().id;//优化,直接用优先队列取最大值 
		q.pop();
		if(vis[u])//已经访问,跳过 
			continue;
		vis[u]=1;//标记已经访问 
		//选出了当前最小距离,以u号结点往外松弛 
		for(int i=head[u];i;i=nxt[i]){
			v=to[i],w=val[i];
			if(dis[v]>dis[u]+w)//更新最短距离 
				dis[v]=dis[u]+w,q.push(node{v,dis[v]});
		}
		vis[u]=1;//u号结点松弛结束(所有与它相邻的点) 
	} 
	for(int i=1;i<=n;i++)//输出距离起点的距离 
		printf("%d ",dis[i]);
	return 0;
} 

总结

感谢大家的观看!有问题可以随时和我反馈,毕竟各位神犇比本蒟蒻厉害多了😁

推荐大家一个临时邮箱吧,比较不错,只是一开始加载较慢:网站

我这里还有挺多好用的资源的,以后每写一篇文章就分享一下吧。

希望各位大神留下一个赞~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值