迪杰斯特拉算法详解

介绍

英文名: Dijkstra

用处: 这是个用来在边有非负权的图中求单源最短路的算法。

(注:单源,即只有一个固定的起点)

流程

说白了,就一句话:每次找到离起点最近的点,然后用它到起点的最短路来更新其他的点到起点的最短路,以及,每个点只用一次。

证明和其他的在下面,这里先举个栗子!

比如说现在有这样一张 5 5 5 个点的图(起点是 1 1 1):
在这里插入图片描述
下面的 d i s dis dis 数组表示现在每个点到起点的距离,因为其他点一开始都不知道,所以我们需要初始化成一个比较大的数。

那么一开始离起点最近的肯定就是起点自己啦,于是用点 1 1 1 进行一次更新,点 2 , 3 2,3 2,3 会被更新。
在这里插入图片描述
为了方便,我们将 1 1 1 号点标成绿色,表示这个点已经用过了。

那么现在能用的点有: 2 , 3 , 4 , 5 2,3,4,5 2,3,4,5,其中离起点最近的是点 3 3 3。然后用点 3 3 3 进行一次更新,点 4 , 5 4,5 4,5 会被更新。
在这里插入图片描述
现在能用的有: 2 , 4 , 5 2,4,5 2,4,5,离起点最近的是点 2 2 2

用点 2 2 2 进行一次更新,点 4 4 4 会被更新,而点 3 3 3 并不会,因为 d i s [ 3 ] < d i s [ 2 ] + 2 dis[3]<dis[2]+2 dis[3]<dis[2]+2
在这里插入图片描述
现在能用的有: 4 , 5 4,5 4,5,离起点最近的是点 4 4 4

用点 4 4 4 进行一次更新,没有点会被更新。

最后用点 5 5 5 也更新不了其他点,于是我们就这样求出了起点到每个点的最短路。


正确性

为什么这样做是对的呢?

其实也很简单,因为边的权值都是非负的,所以我们每次选出的离起点最近的点,它的 d i s dis dis 已经不可能再被别人更新了,因为剩下的 d i s dis dis 都比他大。

限制

这是个 只能 用来在 边有非负权的图 中求 单源最短路 的算法。

没错,不只是负环,连权值都不能为负。

想想那个正确性的证明,只有当权值非负的时候,我们每次选出的离起点最近的点的 d i s dis dis 才不可能被别人更新,假如有负权边,那么它还是可能被更新。

例:
在这里插入图片描述

如果遇上这种情况,那么点 1 1 1 会先更新 2 , 3 2,3 2,3,此时:
d i s : 0 , 2 , 3 , ∞ dis:0,2,3,\infty dis:0,2,3,
然后,点 2 2 2 更新 4 4 4,得到:
d i s : 0 , 2 , 3 , 3 dis:0,2,3,3 dis:0,2,3,3
然后,点 3 3 3 更新 2 2 2,得到:
d i s : 0 , 1 , 3 , 3 dis:0,1,3,3 dis:0,1,3,3

显然 d i s [ 4 ] dis[4] dis[4] 的值不应该是 3 3 3,而应该是 2 2 2

普通的模板

bool v[maxn];//记录每个点是否用过
int dis[maxn];
void dij(int st)
{
	for(int i=0;i<=n;i++)//初始化
	dis[i]=2147483647;
	dis[st]=0;
	for(int i=1,x;i<=n;i++)
	{
		x=0;
		for(int j=1;j<=n;j++)//找出没用过的点里面离起点最近的点
		if(!v[j]&&dis[j]<dis[x])x=j;
		v[x]=true;//标记为用过
		
		for(int j=first[x];j;j=e[j].next)//更新其他点
		if(dis[e[j].y]>dis[x]+e[j].z)dis[e[j].y]=dis[x]+e[j].z;
	}
}

模板题

AC代码:

#include <cstdio>
#include <cstring>
#define maxn 10010

int n,m,qidian;
struct edge{int y,z,next;};
edge e[1000010];
int first[maxn],len=0;
void buildroad(int x,int y,int z)
{
	e[++len]=(edge){y,z,first[x]};
	first[x]=len;
}
bool v[maxn];
int dis[maxn];
void dij(int st)
{
	for(int i=0;i<=n;i++)
	dis[i]=2147483647;
	dis[st]=0;
	for(int i=1,x;i<=n;i++)
	{
		x=0;
		for(int j=1;j<=n;j++)
		if(!v[j]&&dis[j]<dis[x])x=j;
		v[x]=true;
		
		for(int j=first[x];j;j=e[j].next)
		if(dis[e[j].y]>dis[x]+e[j].z)dis[e[j].y]=dis[x]+e[j].z;
	}
}

int main()
{
	scanf("%d %d %d",&n,&m,&qidian);
	for(int i=1,x,y,z;i<=m;i++)
	scanf("%d %d %d",&x,&y,&z),buildroad(x,y,z);
	dij(qidian);
	for(int i=1;i<=n;i++)
	printf("%d ",dis[i]);
}

我们发现这种不优秀的做法的时间复杂度是 O ( n 2 ) O(n^2) O(n2) 的,这显然不符合它的名声,于是有了下一部分。

堆优化

其实也不是什么奇怪的东西,我们发现,每次要找的都是 d i s dis dis 最小的点,那么我们用堆维护一下最小值即可。

代码如下:

int dis[maxn];
struct par{
	int x;
	par(int xx):x(xx){};
	bool operator <(const par &b)const{return dis[x]==dis[b.x]?(x<b.x):dis[x]<dis[b.x];}
};
set<par> s;
void dij(int st)
{
	for(int i=1;i<=n;i++)
	dis[i]=2147483647;
	dis[st]=0; s.insert(par(st));
	while(!s.empty())
	{
		int x=s.begin()->x; s.erase(par(x));
		for(int i=first[x];i;i=e[i].next)
		{
			int y=e[i].y;
			if(dis[y]>dis[x]+e[i].z)
			s.erase(par(y)),dis[y]=dis[x]+e[i].z,s.insert(par(y));
			//注意,这里一定要先erase,再修改dis[y],不然set里面因为关键字的改变会爆炸
		}
	}
}

模板题加强版

AC代码:

#include <cstdio>
#include <cstring>
#include <set>
#include <algorithm>
using namespace std;
#define maxn 1000010

int n,m,qidian;
struct edge{int y,z,next;};
edge e[maxn<<2];
int first[maxn],len=0;
void buildroad(int x,int y,int z)
{
	e[++len]=(edge){y,z,first[x]};
	first[x]=len;
}
int dis[maxn];
struct par{
	int x;
	par(int xx):x(xx){};
	bool operator <(const par &b)const{return dis[x]==dis[b.x]?(x<b.x):dis[x]<dis[b.x];}
};
set<par> s;
void dij(int st)
{
	for(int i=1;i<=n;i++)
	dis[i]=2147483647;
	dis[st]=0; s.insert(par(st));
	while(!s.empty())
	{
		int x=s.begin()->x; s.erase(par(x));
		for(int i=first[x];i;i=e[i].next)
		{
			int y=e[i].y;
			if(dis[y]>dis[x]+e[i].z)
			s.erase(par(y)),dis[y]=dis[x]+e[i].z,s.insert(par(y));
		}
	}
}

int main()
{
	scanf("%d %d %d",&n,&m,&qidian);
	for(int i=1,x,y,z;i<=m;i++)
	scanf("%d %d %d",&x,&y,&z),buildroad(x,y,z);
	dij(qidian);
	for(int i=1;i<=n;i++)
	printf("%d ",dis[i]);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值