图——最短路径的两种算法

最短路径

在生活中,例如,当我们坐公交或轻轨时,都会看一下交通图,找到在哪个站下是最快能达到目的地的,也就是路径最小。考虑到交通图的有向性,例如汽车的上山下山、轮船的顺水逆水,所花费的时间或代价就不相同,所以交通网往往是用带权的有向网表示。在带权的有向网中,习惯上称路径上的第一个顶点称为源点(Source),最后一个顶点称为终点(Destination)

下面介绍两种最常见的最短路径问题:

  1. 从某个源点到其余各顶点 的最短路径
  2. 每一对顶点之间的最短路径

源点 → \rightarrow 其余各顶点(迪杰斯特拉算法)

迪杰斯特拉(Dijkstra)提出了一个按路径长度递增的次序产生最短路径 的算法。

1. 求解过程

对于网 N = ( V , E ) N=(V,E) N=(V,E),将N中的顶点分为两组(同MST性质一样):
S:已求出最短路径所包含的顶点
V-S:未求出的最短路径的顶点

该求解过程也就是按各顶点与 v 0 v_{0} v0间最短路径长度递增的次序,逐个将V-S中的顶点加入到S中

例如,对于下图求解其 v 0 v_{0} v0的最小路径
在这里插入图片描述
根据迪杰斯特拉算法的求解过程,首先求出 v 0 v_{0} v0 v 2 v_{2} v2的路径 < v 0 , v 2 > <v_{0},v_{2}> <v0,v2>,然后按路径长度递增的次序依次得到 v 0 v_{0} v0 v 4 v_{4} v4的路径 < v 0 , v 4 > <v_{0},v_{4}> <v0,v4> v 0 v_{0} v0 v 3 v_{3} v3的路径 < v 0 , v 4 , v 3 > <v_{0},v_{4},v_{3}> <v0,v4,v3> v 0 v_{0} v0 v 5 v_{5} v5的路径 < v 0 , v 4 , v 3 , v 5 > <v_{0},v_{4},v_{3},v_{5}> <v0,v4,v3,v5>,而 v 0 v_{0} v0 v 1 v_{1} v1没有路径。

或许用下面的表格来表示更加清晰:

源点终点最短路径路径长度
v 0 v_{0} v0 v 2 v_{2} v2 < v 0 , v 2 > <v_{0},v_{2}> <v0,v2>10
v 0 v_{0} v0 v 4 v_{4} v4 < v 0 , v 4 > <v_{0},v_{4}> <v0,v4>30
v 0 v_{0} v0 v 3 v_{3} v3 < v 0 , v 4 , v 3 > <v_{0},v_{4},v_{3}> <v0,v4,v3>50
v 0 v_{0} v0 v 5 v_{5} v5 < v 0 , v 4 , v 3 , v 5 > <v_{0},v_{4},v_{3},v_{5}> <v0,v4,v3,v5>60
v 0 v_{0} v0 v 1 v_{1} v1 ∞ \infty

熟悉了该算法的求解过程后,下面来实现该算法的代码

2. 算法实现

算法分析:
在这里我们对于无向网仍用邻接矩阵的存储表示,源点为 v 0 v_{0} v0

  1. 首先,我们想到的是最短路径一定有一个值,那么我们可以用一维数组来存储当前 v 0 v_{0} v0到其余各顶点的最短路径的值,命名该数组为 D [ i ] D[i] D[i]。若就 v 0 v_{0} v0 v i v_{i} vi之间没有弧,则 D [ i ] = ∞ D[i]=\infty D[i]=。!!注意:该数组的初始化也被赋于就是 v 0 v_{0} v0 v i v_{i} vi之间的弧的权值,且默认该值就是当前的最小路径,对于后续操作,我们会对该数组中的值进行更新,通过比较来取得更小的路径。
  2. 其次,我们对于这些算法大都有循环结构,那么我们就需要直到 v 0 v_{0} v0 v i v_{i} vi的最短路径是否已被确定。我们对此也可以用一个一维数组 S [ i ] S[i] S[i]来记录,若已确定,则 S [ i ] = t u r e S[i]=ture S[i]=ture,否则 S [ i ] = f a l s e S[i]=false S[i]=false
  3. 那么还有一个问题需要我们考虑的是,若遇到上图 v 0 v_{0} v0 v 1 v_{1} v1没有路径的情况该怎么办。我们需要记录其余顶点的直接前趋是否存在,也可以用一个一维数组 P a t h [ i ] Path[i] Path[i]来存储该顶点的直接前趋的顶点序号。例如,若 v 0 v_{0} v0 v i v_{i} vi若有弧,则 P a t h [ i ] = v 0 Path[i]=v_{0} Path[i]=v0,否则 P a t h [ i ] = − 1 Path[i]=-1 Path[i]=1
  4. 每当加入一个新的顶点到S中,则该顶点可作为一个“中转站”。例如,对于上述无向网,假如此时S中已有 v 0 v_{0} v0 v 2 v_{2} v2,若要求 v 0 v_{0} v0 v 3 v_{3} v3的最小路径,那么首先我们已有 v 0 v_{0} v0直接到 v 3 v_{3} v3的路径(也就是只有一条弧的路径) D [ 3 ] D[3] D[3],然后再将 v 0 → v 2 → v 3 v_{0}\rightarrow v_{2}\rightarrow v_{3} v0v2v3的路径 D [ 2 ] + a r c s [ 2 ] [ 3 ] D[2]+arcs[2][3] D[2]+arcs[2][3] D [ 3 ] D[3] D[3]比较,若中转后的路径比原来的路径小,则将中转后的路径替换原来的 D [ 3 ] D[3] D[3]
  5. 直到所有顶点都加入到S中为止。

算法步骤:

  1. 首先是各数组的初始化,从源点 v 0 v_{0} v0出发,将 v 0 v_{0} v0加入到S中,并使 S [ v 0 ] = t r u e S[v_{0}]=true S[v0]=true
  2. v 0 v_{0} v0到其余各顶点的最短路径长度初始化为权值,即 D [ i ] = G . a r c s [ v 0 ] [ v i ] D[i]=G.arcs[v_{0}][v_{i}] D[i]=G.arcs[v0][vi]
  3. 再判断 v 0 v_{0} v0 v i v_{i} vi之间是否有弧,若有,则将 v i v_{i} vi的前趋置为 v 0 v_{0} v0,即 P a t h [ i ] = v 0 Path[i]=v_{0} Path[i]=v0,否则 P a t h [ i ] = − 1 Path[i]=-1 Path[i]=1
  4. 初始化结束之后,我们就该用循环结构来寻求最短路径(循环n-1次):
  • 选择下一条最短路径的终点 v k v_{k} vk,将 v k v_{k} vk加入到S中,并且使 S [ v k ] = t r u e S[v_{k}]=true S[vk]=true
  • 根据条件更新从 v 0 v_{0} v0出发到剩下的任一顶点(V-S中的顶点)的最短路径,若 D [ k ] + G a r c s [ k ] [ i ] < D [ i ] D[k]+Garcs[k][i]<D[i] D[k]+Garcs[k][i]<D[i]成立,则更新 D [ i ] = D [ k ] + G . a r c s [ k ] [ i ] D[i]=D[k]+G.arcs[k][i] D[i]=D[k]+G.arcs[k][i],同时更改 v i v_{i} vi的前趋为 v k v_{k} vk,即 P a t h [ i ] = k Path[i]=k Path[i]=k

有了前面的分析和步骤,下面给出具体的代码:

3. 具体代码

这里以上述的有向网G6为例(默认 v 0 → v 5 v_{0}\rightarrow v_{5} v0v5的下标为0~5):

#define MaxInt 32767

void ShortestPath_DIJ(AMGraph G,int v0)
{
	int v=0;


	//初始化
	for(int v=0;v<G.vexnum;v++)
	{
		S[v]=false;
		D[v]=G.arcs[v0][v];
		if(D[v]<MaxInt)
			Path[v]=v0;
		else
			Path[v]=-1;		
	}
	
	S[v0]=true;
	D[v0]=0;
	
	
	//开始求最短路径
	for(int i=0;i<G.vexnum;i++)
	{
		min=MaxInt;
		for(int w=0;w<G.vexnum;w++)							//找出当前的最短路径
			if(!S[w]&&D[w]<min)
			{
				v=w;
				min=D[w];
			}
		S[v]=true;
		for(int w=0;w<G.vexnum;w++)							//更新D[w]
			if(!S[w]&&(D[v]+G.arcs[v][w]<D[w]))
			{
				D[w]=G.arcs[v][w];
				Path[w]=v;
			}
	}
}

简单概括一下:首先我们从D[i]中选出最短的路径,将该顶点加入到S中,此时S中有 v 0 , v 2 v_{0},v_{2} v0,v2(假设新加入的顶点是 v 2 v_{2} v2),然后进行D[i]更新,此时 v 2 v_{2} v2作为中转站,若经过 v 2 v_{2} v2再到 v 3 v_{3} v3(剩余顶点中的一个,这里只是举一个具体的例子,全部顶点都要进行比较)的路径比 v 0 v_{0} v0直接到 v 3 v_{3} v3的路径短,则更新D[3]。然后又从更新后的D[i]中选出路径最小的,又将该顶点(假定是 v 4 v_{4} v4)加入S中,此时S中有 v 0 , v 2 , v 4 v_{0},v_{2},v_{4} v0,v2,v4,然后又更新D[i],此时 v 4 v_{4} v4作为中转站(注意,此时D[i]中的值可能已经在 v 2 v_{2} v2中转过),假如 v 3 v_{3} v3已在 v 2 v_{2} v2中转过,那么此时D[3]的值就为路径 v 0 → v 2 → v 3 v_{0}\rightarrow v_{2}\rightarrow v_{3} v0v2v3的权值和,若再在 v 4 v_{4} v4中转后到 v 3 v_{3} v3的路径(即 v 0 → v 2 → v 4 → v 3 v_{0}\rightarrow v_{2}\rightarrow v_{4}\rightarrow v_{3} v0v2v4v3)比D[3]小,那么就对其进行更新…

顶点 → \rightarrow 顶点(弗洛伊德算法)

求解各顶点间的最短路径在前面的基础上就比较简单了,我们可以调用n次迪杰斯特拉算法求得n个顶点分别到其余顶点的最短路径。但这个方法形式上比较复杂。下面介绍 弗洛伊德(Floyd) 算法来解决这一问题,两种算法的时间复杂度都为 O ( n 3 ) O(n^3) O(n3),但弗洛伊德算法在形式上更为简单,更容易理解。

1. 算法实现

我们仍用带权的邻接矩阵来表示有向网,同时引入一下辅助数组:

  1. P a t h [ i ] [ j ] : Path[i][j]: Path[i][j]:最短路径上顶点 v j v_{j} vj的前一顶点的序号。
  2. D [ i ] [ j ] : D[i][j]: D[i][j]:记录顶点 v i v_{i} vi v j v_{j} vj之间的最短路径长度。

算法分析:

  1. 该算法实际上十分容易理解,第一步还是先对最短路径长度数组和前趋数组进行初始化
  2. 初始化完成后,就是比较并更新了,对于两个不相同的顶点 v i v_{i} vi v j v_{j} vj,若在它们之间路径增加一个顶点 v k v_{k} vk,若 v i → v k v_{i}\rightarrow v_{k} vivk加上 v k → v j v_{k}\rightarrow v_{j} vkvj的路径小于 v i → v j v_{i}\rightarrow v_{j} vivj的路径,则对 D [ i ] [ j ] D[i][j] D[i][j]进行更新,并更新 P a t h [ i ] [ j ] = P a t h [ k ] [ j ] Path[i][j]=Path[k][j] Path[i][j]=Path[k][j]。当在 v i v_{i} vi v j v_{j} vj已试过增加其余各个顶点后,那么就改变 j j j的值,再进行一轮循环增加中间顶点。当 j j j已全部试过后,就开始改变 i i i的值,综上,也就是说这是一个循环含有一个内循环和一个内循环的内循环,也就是三重循环。

2. 具体代码

void ShortestPath_Floyd(AMGraph G)
{
	//初始化
	for(int i=0;i<G.vexnum;i++)
		for(int j=0;i<G.vexnum;j++)
		{
			D[i][j]=G.arcs[i][j];
			if(D[i][j]<MaxInt&&i!=j)			//两部相同顶点之间有弧
				Path[i][j]=i;
			else 
				Path[i][j]=-1;
		}
	
	//更新D[i][j]数组
	for(int k=0;k<G.vexnum;k++)
		for(int i=0;i<G.vexnum;i++)
			for(int j=0;j<G.vexnum;j++)
				if(D[i][k]+D[k][j]<D[i][j])
				{
					D[i][j]=D[i][k]+D[k][j];
					Path[i][j]=Path[k][j];		//j的前趋变成k
				}
}

总结

关于最短路径的两种算法,各自都有方便之处,假设我们在玩王者荣耀,每次复活后,从水晶出发,都想以最快的速度到达敌方的任一一座防御塔,那么此时我们就可以用迪杰斯特拉算法来实现这个想法。如果我们在不死亡的情况,可以出现在地图任一一座防御塔旁,此时我们需要最快地到达另一座防御塔,此时也可以用迪杰斯特拉算法,但弗洛伊德算法相较于迪杰斯特拉算法结构上更简单,更方便实现。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值