图的最短路入门

图的最短路入门


讲到图论,不得不提及信息学竞赛中最常考的最短路算法,最短路算法已经成为几乎每位选手都必会的算法了,本文将介绍3种最短路算法:Floyd,Dijkstra,SPFA



最短路


定义

对在权图G=(V,E),从一个源点s到汇点t有很多路径,其中路径上权和最少的路径,称从s到t的最短路径。

比如给出这样一幅图G:
在这里插入图片描述
v 2 v_2 v2 v 5 v_5 v5的最短路径就是 v 2 ⇒ v 4 ⇒ v 1 ⇒ v 5 v_2\Rightarrow v_4 \Rightarrow v_1 \Rightarrow v_5 v2v4v1v5,最短路的权值为 3 + 1 + 2 = 6 3+1+2=6 3+1+2=6


思考

是不是所有的图都有最短路呢?显然不是,为什么?我们将图改一下,看图:
在这里插入图片描述
有些人认为:这很简单啊!最短路径还是刚才的那一条,权值是 − 3 + 1 + 2 = 0 -3+1+2=0 3+1+2=0
真的是吗?再仔细看看,可以发现 v 2 , v 3 , v 4 v_2,v_3,v_4 v2,v3,v4组成了一个环,且两两之间的边权值为 − 3 -3 3
那我的最短路径不就是 v 2 ⇒ v 3 ⇒ v 4 ⇒ v 2 ⇒ v 3 ⇒ v 4 ⋯ ⇒ v 1 ⇒ v 5 v_2\Rightarrow v_3 \Rightarrow v_4 \Rightarrow v_2 \Rightarrow v_3 \Rightarrow v_4 \cdots \Rightarrow v_1 \Rightarrow v_5 v2v3v4v2v3v4v1v5了吗?
因为这样的权值就是 − 3 − 3 − 3 − 3 − 3 ⋯ + 1 + 2 -3-3-3-3-3\cdots+1+2 33333+1+2,不断加 − 3 -3 3权值不断减小!
而且为了路径最短,我们会在这个负环当中走无数次,从而就无法求出最短路了,如果你非要求最短路,你的程序会陷入死循环!
所以在有负环的图中不存在最短路


求最短路的核心思想与重要性质

松弛操作

松弛操作是求 a , b a,b a,b间的最短路时,通过寻找一个中间点 k k k,使得 d i s ( a , k ) + d i s ( k , b ) < d i s ( a , b ) dis(a,k)+dis(k,b)<dis(a,b) dis(a,k)+dis(k,b)<dis(a,b) 1,从而实现对 a , b a,b a,b间最短路的更新,看图!
在这里插入图片描述
多么奇葩的三角形(两边之和小于第三边!)
多么优美的三角关系(Green Hat)

求最短路时,我们通常使用松弛操作,实现最短路最短。
其本质在于:

三角不等式性质: ∀ ( u , v ) ∈ E , δ ( s , v ) ≤ δ ( s , u ) + w ( u , v ) \forall (u,v) \in E,\delta(s,v) \leq\delta(s,u)+w(u,v) (u,v)E,δ(s,v)δ(s,u)+w(u,v)

最短路径的最优子结构

最短路重要性质:两个结点点之间的一条最短路径包含其他的最短路径。
在大部分最短路中,贪心和动态规划都是正确的思想,因为求最短路的这些图大部分都是只有距离和代价,而没有收益,不像背包问题等动态规划的问题,既有收益,又有代价。
除此以外,更多是因为有了松弛操作和最短路的重要性质,所以才能使得贪心的策略是正确的。

最短路径的子路径也是最短路径
证明:

  • 如果从 i i i j j j有最短路径 p v i → v j p_{v_i\rightarrow v_j} pvivj分解为 ( v i , v a ) , ( v a , v b ) … ( v k , v j ) (v_i,v_a),(v_a,v_b)\dots (v_k,v_j) (vi,va),(va,vb)(vk,vj),其权值则为 w ( i , j ) = w ( i , a ) + w ( a , b ) + ⋯ + w ( k , j ) w(i,j)=w(i,a)+w(a,b)+\dots+w(k,j) w(i,j)=w(i,a)+w(a,b)++w(k,j)
  • 现在假设存在一条路径 p v a → v b ′ p'_{v_a\rightarrow v_b} pvavb其权值 w ′ ≤ w ( a , b ) w'\leq w(a,b) ww(a,b),使得原最短路径 p v i → v j p_{v_i\rightarrow v_j} pvivj的子路径 ( v a , v b ) (v_a,v_b) (va,vb)不是最短路径,那么从 i i i j j j路径 p v i → v j ′ p'_{v_i\rightarrow v_j} pvivj ( v i , v a ) , p v a → v b ′ … ( v k , v j ) (v_i,v_a),p'_{v_a\rightarrow v_b}\dots (v_k,v_j) (vi,va),pvavb(vk,vj)组成。
  • 此时显然可知:其新的权值 w ′ ( i , j ) ≤ w ( i , j ) w'(i,j)\leq w(i,j) w(i,j)w(i,j) p v i → v j p_{v_i\rightarrow v_j} pvivj不是最短路,与条件:从 i i i j j j有最短路径 p v i → v j p_{v_i\rightarrow v_j} pvivj 相矛盾。
  • 所以存在一条路径 p v a → v b ′ p'_{v_a\rightarrow v_b} pvavb其权值 w ′ ≤ w ( a , b ) w'\leq w(a,b) ww(a,b),使得原最短路径 p v i → v j p_{v_i\rightarrow v_j} pvivj的子路径 ( v a , v b ) (v_a,v_b) (va,vb)不是最短路径的假设不成立
  • 所以最短路径的子路径也是最短路径


Floyd—— 像极了 DP

性质
  • Floyd算法可以求出任意两点之间的最短路
  • 不能跑负权图
核心思想

动态规划,大家可以结合松弛操作,尝试着推出递推式。
在松弛操作中,求两点间的最短路是要通过寻找一个中间点 k k k 的,所以我们不妨直接将松弛操作的式子抄一遍:
设现在要求 i , j i,j i,j间的最短路,设 f [ i ] [ j ] f[i][j] f[i][j]表示 i , j i,j i,j间的最短路,有 f [ i ] [ j ] = m i n ( f [ i ] [ j ] , f [ i ] [ k ] + f [ k ] [ j ] ) f[i][j]=min(f[i][j],f[i][k]+f[k][j]) f[i][j]=min(f[i][j],f[i][k]+f[k][j])
这样就刚好对应松弛操作的式子了,因为我们知道松弛操作是可以实现对图中最短路进行更新的,所以据此推导出来的递推式也是正确的。

算法实现
  1. 初始化 f [ a ] [ b ] = m a p [ a ] [ b ] , e ( a , b ) ∈ E f[a][b]=map[a][b],e(a,b)\in E f[a][b]=map[a][b],e(a,b)E a , b a,b a,b之间有一条边)
        f [ x ] [ y ] = ∞ , e ( x , y ) ∉ E f[x][y]=\infty,e(x,y)\notin E f[x][y]=,e(x,y)/E x , y x,y x,y之间没有边)
  2. 枚举中间点 k k k
  3. 枚举起点 i i i
  4. 枚举终点 j j j
  5. 松弛操作 f [ i ] [ j ] = m i n ( f [ i ] [ j ] , f [ i ] [ k ] + f [ k ] [ j ] ) f[i][j]=min(f[i][j],f[i][k]+f[k][j]) f[i][j]=min(f[i][j],f[i][k]+f[k][j])

代码如下:

for(int k=1;k<=n;k++)
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			if(i!=j)
				f[i][j]=min(f[i][j],f[i][k]+f[k][j]);
cout<<f[x][y]<<endl;//输出x->y的最短路

时间复杂度不堪入目 O ( ∣ V ∣ 3 ) O(|V|^3) O(V3)2
我知道很多人会问:为什么先枚举中间点 k k k自己去看吧因为我也没研究过。。。



Dijkstra—— 真正的贪心

性质
  • Dijkstra是用于求单源最短路的,也就是一个起点, n − 1 n-1 n1个终点
  • 不能跑带负权的图!!!
核心思想

从起点出发,用松弛操作更新与现在所在的点到相邻的所有点的距离,下一次来到一个与起点距离最短且没有访问过的点继续更新,循环至所有点都被访问过了为止。是不是听的一脸蒙蔽 看图!

比如说现在有这么一个图,起点为 1 1 1,终点为 7 7 7,设 d ( i ) d(i) d(i)表示从 1 1 1 i i i的最短路径
在这里插入图片描述
现在我们从起点开始做Dijkstra算法

d ( 1 ) d(1) d(1) d ( 2 ) d(2) d(2) d ( 3 ) d(3) d(3) d ( 4 ) d(4) d(4) d ( 5 ) d(5) d(5) d ( 6 ) d(6) d(6) d ( 7 ) d(7) d(7)
0 0 0 ∞ \infty ∞ \infty ∞ \infty ∞ \infty ∞ \infty ∞ \infty

在这里插入图片描述
我们访问过 1 1 1后,更新与它相邻的点

d ( 1 ) d(1) d(1) d ( 2 ) d(2) d(2) d ( 3 ) d(3) d(3) d ( 4 ) d(4) d(4) d ( 5 ) d(5) d(5) d ( 6 ) d(6) d(6) d ( 7 ) d(7) d(7)
0 0 0 3 3 3 2 2 2 ∞ \infty ∞ \infty ∞ \infty ∞ \infty

下一步,找离 1 1 1最近的点 3 3 3
在这里插入图片描述
更新与 3 3 3相邻的点

d ( 1 ) d(1) d(1) d ( 2 ) d(2) d(2) d ( 3 ) d(3) d(3) d ( 4 ) d(4) d(4) d ( 5 ) d(5) d(5) d ( 6 ) d(6) d(6) d ( 7 ) d(7) d(7)
0 0 0 3 3 3 2 2 2 5 5 5 ∞ \infty 10 10 10 ∞ \infty

下一步,找离 1 1 1最近的点 2 2 2
在这里插入图片描述
更新与 2 2 2相邻的点

d ( 1 ) d(1) d(1) d ( 2 ) d(2) d(2) d ( 3 ) d(3) d(3) d ( 4 ) d(4) d(4) d ( 5 ) d(5) d(5) d ( 6 ) d(6) d(6) d ( 7 ) d(7) d(7)
0 0 0 3 3 3 2 2 2 5 5 5 7 7 7 10 10 10 ∞ \infty

下一步是点 4 4 4
在这里插入图片描述

d ( 1 ) d(1) d(1) d ( 2 ) d(2) d(2) d ( 3 ) d(3) d(3) d ( 4 ) d(4) d(4) d ( 5 ) d(5) d(5) d ( 6 ) d(6) d(6) d ( 7 ) d(7) d(7)
0 0 0 3 3 3 2 2 2 5 5 5 7 7 7 7 7 7 ∞ \infty

下一步是点 5 5 5
在这里插入图片描述

d ( 1 ) d(1) d(1) d ( 2 ) d(2) d(2) d ( 3 ) d(3) d(3) d ( 4 ) d(4) d(4) d ( 5 ) d(5) d(5) d ( 6 ) d(6) d(6) d ( 7 ) d(7) d(7)
0 0 0 3 3 3 2 2 2 5 5 5 7 7 7 7 7 7 8 8 8

下一步是点 6 6 6
在这里插入图片描述

d ( 1 ) d(1) d(1) d ( 2 ) d(2) d(2) d ( 3 ) d(3) d(3) d ( 4 ) d(4) d(4) d ( 5 ) d(5) d(5) d ( 6 ) d(6) d(6) d ( 7 ) d(7) d(7)
0 0 0 3 3 3 2 2 2 5 5 5 7 7 7 7 7 7 8 8 8

最后一步点 7 7 7没有什么意义了
在这里插入图片描述

d ( 1 ) d(1) d(1) d ( 2 ) d(2) d(2) d ( 3 ) d(3) d(3) d ( 4 ) d(4) d(4) d ( 5 ) d(5) d(5) d ( 6 ) d(6) d(6) d ( 7 ) d(7) d(7)
0 0 0 3 3 3 2 2 2 5 5 5 7 7 7 7 7 7 8 8 8

经历千辛万苦,终于把它给模拟完了,不是很详细,但主要是看图(图没看懂可以尝试看这个),可以发现蓝点都是被访问过了的,就不必再访问,白点负责算法的下一步更新,这就是许多人说的**“蓝白点”思想**,而且可以发现找下一个点去更新的时候,那是一丝不挂 赤裸裸的贪心
总结一下:“蓝白点”+贪心

算法实现

我的图是不是放错位置了,请大家再看一遍上面的模拟,自行总结算法步骤!

  1. 找离起点最近的白点 u u u
  2. 用松弛操作更新与 u u u相邻的点 v v v d ( v ) = m i n ( d ( v ) , d ( u ) + w ( u , v ) ) d(v)=min(d(v),d(u)+w(u,v)) d(v)=min(d(v),d(u)+w(u,v)) w ( u , v ) w(u,v) w(u,v) u u u v v v的边权
  3. u u u打上标记,划为“蓝点”,不再访问
  4. 回到步骤1

代码如下:

memset(d,inf,sizeof(d));//初始化
d[start]=0;
while(1)
{
	int u=-1;
	for(int i=1;i<=n;i++)//寻找白点
		if(!vis[i]&&(u==-1||d[i]<d[u]))//寻找离起点最近的白点
			u=i;
	if(u==-1) break;//没有白点了
	vis[u]=1;//打上标记,划为蓝点,不再访问
	for(int i=head[u];i!=0;i=e[i].next)//更新相邻的点
		d[e[i].to]=min(d[e[i].to],d[u]+e[i].dis);//松弛操作
}
cout<<d[target]<<endl;//输出起点->target的最短路

时间复杂度 O ( ∣ V ∣ 2 ) O(|V|^2) O(V2)虽然比Floyd快,但还是不任直视
优化 你不早说,我刚刚那句话很尴尬啊!
这一个算法总体而言有两部分:

  1. 找点
  2. 更新
    我们注意到,找点它是找离起点最接近的点,所以根据这个“最”字我们可以找到突破口排序?
    直接用数据结构优化: 删除线君当场阵亡
    用小根堆维护白点,每次取点时,取堆顶的元素即为离起点最近的白点:
priority_queue<pair<int,int> > q;
//大根堆(优先队列),pair的第二维为结点编号,第一维为dis的相反数(利用相反数变小根堆)
void Dijkstra()
{
	memset(d,0x3f,sizeof(d));
	memset(vis,0,sizeof(vis));
	d[1]=0;
	q.push(make_pair(0,1));
	while(!q.empty())
	{
		int u=q.top().second;//取堆顶
		q.pop();
		if(vis[u]) continue;//不访问蓝点
		vis[u]=1;
		for(int i=head[u];i!=0;i=e[i].next)//更新
		{
			int v=e[i].to;int w=e[i].dis;
			if(d[v]>d[u]+w)//松弛操作
			{
				d[v]=d[u]+w;
				q.push(make_pair(-d[v],v));//元素插入堆
			}
		}
	}
}

时间复杂度 O ( ( ∣ V ∣ + ∣ E ∣ ) log ⁡ ∣ E ∣ ) O((|V|+|E|)\log |E|) O((V+E)logE)3,已经算是十分高效了。



SPFA—— 人家其实叫“队列优化的Bellman-Ford”算法啦

原论文4

性质
  • 用于求单源最短路,爸爸本质是Bellman-Ford算法,请自行了解。
  • 可以判断负环。
  • 可以跑负权图。
核心思想

迭代求解,队列优化
迭代求解:每操作完一个点,对所有相邻的点进行下一步操作,反复对边进行松弛操作,确保得到最优解
队列优化:将相邻的点入队,下一步直接取队头,而不用再枚举所有的点
除了这两点与Dijkstra不同外,其他相同。

算法实现
  1. 取队头,获得当前的位置 u u u
  2. 枚举所有出边,做一遍松弛操作
  3. 如果某条边 e ( u , v ) e(u,v) e(u,v)可以做松弛操作,检查 v v v是否入队,如果没有,就将 v v v入队了
  4. 回到步骤1,直至队列取空
    代码如下:
void SPFA()
{
	queue<int > q;
	memset(d,inf,sizeof(d));//初始化
	memset(vis,0,sizeof(vis));
	d[s]=0;vis[s]=1;
	q.push(s);//起点入队
	while(!q.empty())//循环直至队列为空
	{
		int u=q.front();//取队头
		q.pop();
		vis[u]=0;//取消标记
		for(int i=head[u];i!=0;i=e[i].next)//枚举出边
		{
			int v=e[i].to,w=e[i].dis;
			if(d[v]>d[u]+w)//判断能否松弛
			{
				d[v]=d[u]+w;
				if(!vis[v])//判断是否入队
				{
					vis[v]=1;
					q.push(v);//入队
				}
			}
		}
	}
	cout<<d[target]<<endl;
}

最好情况下的时间复杂度 O ( k ∣ E ∣ ) O(k|E|) O(kE) k k k是个较小的常数,原论文中有详细证明
最坏情况下退化为 O ( ∣ V ∣ ∣ E ∣ ) O(|V||E|) O(VE)
大家还可以自行了解SPFA的进一步优化
《SPFA算法的优化及应用》5



总结

三种基础的最短路算法讲完了。
大家在做图论求最短路的题目时,一定要看清楚题目所给的条件,精挑细选,选出最合适的算法去做,否则,会带来毁灭性 截然不同的结果。
最短路的题目还常常与其他算法混着考,所以大家一定要多做点题,先熟悉最短路算法,深谙其性质特征,再拓展一些算法混考的题目,这样就能够比较轻松地应付OI竞赛了!


  1. d i s ( x , y ) dis(x,y) dis(x,y)表示 x x x y y y的距离, d i s dis dis d i s t a n c e distance distance距离的缩写 ↩︎

  2. ∣ V ∣ |V| V代表图中顶点的个数 ↩︎

  3. ∣ E ∣ |E| E代表图中的边数 ↩︎

  4. 西南交通大学段凡丁《关于最短路径的SPFA快速算法》 ↩︎

  5. 广东中山纪念中学姜碧野《SPFA算法的优化及应用》 ↩︎

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值