最短路径算法总结

最短路径算法(Shortest Path Algorithms)总结

Time:20210306 Author: BJTU/PacificL


文章目录

1.弗洛伊德算法 (Floyd - Warshall)

参考文献:Floyd - Warshall(弗洛伊德算法)

1. 简介

Floyd算法由算法创始人之一,1978年图灵奖获得者、斯坦福大学计算机科学系教授罗伯特·弗洛伊德命名的名字所命名。而 Floyd算法又被称为“插点法”,是一种利用动态规划的思想寻找给定的加权图中多源点之间最短路径的算法。

2. 问题描述

小P同学需要在四座城市之间往返旅行,有些城市之间有公路,有些城市之间则没有公路,且注意所有的公路都是单向的。求怎样才能求出任意两个城市之间的最短路径?

在这里插入图片描述

3. 思路阐述

1. 初始距离矩阵
D1234
10264
2 ∞ \infty 03 ∞ \infty
37 ∞ \infty 01
45 ∞ \infty 120

上面展示的就是任意两个城市之间的距离矩阵 D D D,我们用行数 i i i 和列数 j j j 分别表示起始城市和目的城市,例如,从城市3到城市1的距离为 D [ 3 ] [ 1 ] = 7 D[3][1]=7 D[3][1]=7

这里可以看到,其实并非直接从起始城市到目的城市的的距离是最短的,两个城市之间可以通过中间加入一个或多个城市当做中转城市来使得距离进一步缩短。

2. 一个中转城市的距离矩阵

假设,我们这里只允许城市1作为中转城市,则可以通过判断从起始城市到城市1,,再从城市1到目的城市的距离是否小于直接从起始城市到目的城市的距离来更新距离矩阵 D D D,即 D [ i ] [ 1 ] + D [ 1 ] [ j ] > D [ i ] [ j ] D[i][1]+D[1][j]>D[i][j] D[i][1]+D[1][j]>D[i][j] 时,更新 D [ i ] [ j ] = D [ i ] [ 1 ] + D [ 1 ] [ j ] D[i][j]=D[i][1]+D[1][j] D[i][j]=D[i][1]+D[1][j]

更新之后的距离矩阵 D 1 D_{1} D1 表示如下。

D 1 D_1 D11234
10264
2 ∞ \infty 03 ∞ \infty
37901
457110

表格中加粗的数字为本次更新的距离。

3. 两个或两个以上的中转城市

无论是两个还是两个以上的额中转城市,其本质和上一步是一样的。比如,这次只允许城市1和城市2作为中转城市。我们在上一步的基础上得到城市1作为中转城市的距离矩阵 D 1 D_{1} D1。在此基础上,我们在将城市2作为中转城市,并更新距离矩阵 D 1 D_{1} D1

D 2 D_2 D21234
10254
2 ∞ \infty 03 ∞ \infty
37901
457100

表格中加粗的数字为本次更新的距离。

同理,多个城市如法炮制即可。最终我们将所有的城市都作为中转城市,可以得到一张任意两个城市之间的最短距离矩阵 D f i n a l D_{final} Dfinal

D f i n a l D_{final} Dfinal1234
10254
29034
36801
457100

这就是Floyd算法的整个流程。

4. 总结分析

其实就像简介中说的那样,Floyd算法也称之为“插点法”。正是通过不断地在起始城市和目的城市之间插入中转城市,才逐步得到最短距离矩阵。

优点:比较容易容易理解,可以算出任意两个节点之间的最短距离,代码编写简单。

缺点:时间复杂度比较高 O ( n 3 ) O(n^3) O(n3),不适合计算大量数据。


2. 迪杰斯特拉算法(Dijkstra )

参考文献:算法 7:Dijkstra 最短路算法

1. 简介

Dijkstra算法是由荷兰计算机科学家狄克斯特拉于1959 年提出,是从一个顶点到其余各顶点的最短路径算法,解决的是有权图中最短路径问题。

相比较于上面的Floyd算法求任意两个之间的最短路径,这种称之为“多源最短路径”,Dijkstra算法只是计算指定一点到任意一点的最短路径,这种被称为是“单源最短路径”。

2. 问题描述

这里我们假设有6个点。6个点之间的可能会存在一条单向的带有权重的边。我们求从点1到其余个点的最短距离。

在这里插入图片描述

3. 思路阐述

1. 初始距离矩阵和距离向量

我们将任意两点之间的距离,也就是边的权重初始化为一个二维的距离矩阵 D D D

D123456
10112 ∞ \infty ∞ \infty ∞ \infty
2 ∞ \infty 093 ∞ \infty ∞ \infty
3 ∞ \infty ∞ \infty 0 ∞ \infty 5 ∞ \infty
4 ∞ \infty ∞ \infty 401315
5 ∞ \infty ∞ \infty ∞ \infty ∞ \infty 04
6 ∞ \infty ∞ \infty ∞ \infty ∞ \infty ∞ \infty 0

上面表格中所展示的就是任意两个点之间的距离矩阵 D D D,我们用行数 i i i 和列数 j j j 分别表示起点和终点,例如,从点1到点3的距离为 D [ 1 ] [ 3 ] = 12 D[1][3]=12 D[1][3]=12 。而表格中的 ∞ \infty 则表示两点之间没有直接的通路。

问题中,我们要求的是点-1到其余个点的最短距离,因此我们这里把点1到其余各点的距离拿出作为一个向量,方便计算。

123456
dist0112 ∞ \infty ∞ \infty ∞ \infty
2. 找出当前距离最近的点

在给处起点点1的情况下,我们通过查询 d i s t dist dist 可以看到此时距离点1最近的点是点2,距离为1。

不过,这里要注意的是,是否会存在点1通过其他点中转到点2的距离更近呢?答案是不会。因为,无论是将哪一点作为中转点,其出发点永远都是点1,而通过距离向量 d i s t dist dist 可以发现没有比点2更靠近点1的点了。因此,不会存在点1通过中转点到达点2的距离比直接到达点-2的距离更短了。

这里,便是Dijkstra算法的核心思想,相当于广度搜索的树,每一次都在每一层上贪心地寻找距离根节点最短的点。确定之后,将该点固定,然后在下一个距离确定点最短的点,依次迭代计算。这个找出确定点的方法称为“松弛”,相当于从一个确定点点1松弛到两个确定点点1和点2。

123456
dist0112 ∞ \infty ∞ \infty ∞ \infty

表格中加粗的表示本轮的确定点

3. 找出距离最新的确定点最近的点并更新距离向量

在松弛得到两个确定点点1和点2之后,我们现在可以知道点1到点2的最短距离了,但是到其余点的最短距离仍然未知。我们可以查询其余点到确定点点-2的最短路径。

此时的做法和Floyd算法的判断是一样的。通过查询距离矩阵 D D D,我们可以知道从点2出发有两条路径,一条是点2到点3,距离为9,一条是点2到点4,距离为3。因为我们所求的是从点1到其余各点的最短路径,因此要进行判断,看是否点1到点2再到点4的距离小于点1直接到点4的距离。很明显,是的。因此,更新距离矩阵 d i s t dist dist

123456
dist01104 ∞ \infty ∞ \infty

就这样,如法炮制,依次找到距离最新确定点最近的点,并判断是否满足这样的路径小于直接从起点到该点的距离,如果是的话,就更新距离向量 d i s t dist dist ,不是的话,就不需要改变了。最后可以得到点-1到其余各点的最短路径了。

123456
dist01841317

4. 总结分析

可以看出,Floyd算法是动态规划的形式,每次都计算更新某两点之间的距离。而Dijkstra算法是一种贪心算法。每一步都找到最短的路径。不过Dijkstra的复杂度要比Floyd算法要更低点,是 O ( N 2 ) O(N^2) O(N2)


3. 贝尔曼-福特算法 (Bellman - Ford)

参考文献:Bellman-ford(解决负权边)Bellman-Ford算法详讲

1. 简介

正如上文所说,Dijkstra算法无法对负权重的边进行处理。如果某一边的权值为负时,Dijkstra算法在更新时会将失效,选择错误的路径。Dijkstra算法在每一步通过贪心算法选取未被处理的具有最小权值的节点,然后对其的出边进行松弛操作。而贝尔曼-福特算法简单地对所有边进行松弛操作。这样的策略使得Bellman算法比Dijkstra算法适用于更多种类的输入。

同时,Bellman算法最多只需要循环 n − 1 n-1 n1 次,其中 n n n 为点的个数,因为在一个含有 n n n 个顶点的图中,任意两点之间的最短路径最多包含 n − 1 n-1 n1 条边。倘若在第 n n n 次循环还存在某一条边 e ( i , j ) e(i,j) e(i,j) 使得 D i s t [ i ] + w ( i , j ) < D i s t [ j ] Dist[i]+w(i,j)<Dist[j] Dist[i]+w(i,j)<Dist[j] 的情况,则存在负权回路,其中 D i s t Dist Dist 为某点的距离, w ( i , j ) w(i,j) w(i,j) 为边 e ( i , j ) e(i,j) e(i,j) 的权重。

2. 问题描述

存在5个点,一些点之间存在权值可能为负的边,求点-1到其他点的最短路径。

在这里插入图片描述

3. 思路阐述

1. 列出所有边并初始化距离向量

首先列出所有边的信息,以便后面松弛的时候使用。

起点终点权重
232
12-3
155
452
343

然后初始化以点-1为起始点的距离向量 d i s t dist dist,注意开始的时候点-1到其余点的距离都是无穷大的。

12345
dist0 ∞ \infty ∞ \infty ∞ \infty ∞ \infty
2. 依次检查每条边,并对其相应的点进行松弛

这一步就是和Dijkstra算法不同的地方,Dijkstra算法是寻找最短边的点,而Bellman算法是对满足条件的所有点都进行松弛。

例如,对第一条边计算,看是否满足 d i s t [ 2 ] + w ( 2 , 3 ) < d i s t [ 3 ] dist[2]+w(2,3)<dist[3] dist[2]+w(2,3)<dist[3] ,很显然不等式两边都是 ∞ \infty 所以,无法对点-2进行松弛。同理,对第二条边计算, d i s t [ 1 ] + w ( 1 , 2 ) = − 3 < d i s t [ 2 ] = ∞ dist[1]+w(1,2)=-3<dist[2]=\infty dist[1]+w(1,2)=3<dist[2]= 成立,因此对点-2进行松弛,并更新点-2的距离向量。

如法炮制,我们可以得到第一次循环的距离向量 d i s t 1 dist_1 dist1

Loop:112345
dist0-3 ∞ \infty ∞ \infty 5
3. 循环n-1次

将上一步的步骤循环 n − 1 n-1 n1 次,每一次都是对所有边进行计算。最后可以得到点-1到其余各点的最短路径了。

Loop:n-112345
dist0-3-124
4. 检查是否存在负权回路

在无负权回路的情况下,上面的距离向量将不会再变化。倘若再进行迭代发现还存在某一条边 e ( i , j ) e(i,j) e(i,j) 使得 D i s t [ i ] + w ( i , j ) < D i s t [ j ] Dist[i]+w(i,j)<Dist[j] Dist[i]+w(i,j)<Dist[j] 的情况,则存在负权回路。

4. 总结分析

虽然Bellman算法比Dijkstra算法要复杂度高一些,为 O ( ∣ V ∣ ⋅ ∣ E ∣ ) O(|V|\cdot|E|) O(VE),其中 ∣ V ∣ |V| V ∣ E ∣ |E| E 分别是节点和边的数量。但是Bellman算法可以应对各种输入类型,边的权重也可是负值,不过不能存在负权回路。不过在好很多情况中,可能也用不到 n − 1 n-1 n1 个循环就可以得到最短路径了。


4. 最短路径更快算法(Shortest Path Faster Algorithm,SFPA )

参考文献:SPFA 算法详解(最短路径)图论–SPFA算法(单源最短路)

1. 简介

尽管Bellman算法完善了Dijkstra算法,实现了边的权重为负值的处理。但是因为Bellman算法的复杂度过高,因此在结合了队列优化的基础上提出了SPFA算法。其复杂度为== O ( k ⋅ ∣ E ∣ ) O(k\cdot|E|) O(kE)==,其中 k k k 为每个点的平均进队次数,一般情况下 k k k 是一个常数,在稀疏图中小于2,而 E E E 为边的数量。

2. 问题描述

存在5个点,A、B、C、D、E,其中一些点之间存在权重值可能为负的边。求点A到其余各点的最短路径。

在这里插入图片描述

3. 思路阐述

1. 初始距离向量和队列

和Bellman算法一样,第一步依旧是初始化一个无穷大的距离向量 d i s t dist dist

ABCDE
dist0 ∞ \infty ∞ \infty ∞ \infty ∞ \infty

同时,因为SPFA算法是结合了队列,所以我们初始化一个先入先出的队列 q u e u e queue queue

ABCDE
queue10000

其中, 1 1 1 表示该点在队列当中, 0 0 0 表示该点不在队列当中。

2. 队首元素出队松弛其相接点,松弛点进队列

点A出队,并松弛自己的相邻点,判断是否松弛后的距离小于现在的距离。例如,点B与点A相邻,而且 d i s t [ A ] + w ( A , B ) = 3 < d i s t [ B ] = ∞ dist[A]+w(A,B)=3<dist[B]=\infty dist[A]+w(A,B)=3<dist[B]=。因此,点B的距离向量进行更新,同时点B进入队列。同理,点C也是如此。但是,其余的点不满足上式的条件,因此,也不能进队。

更新之后的距离向量和队列如下。

ABCDE
dist032 ∞ \infty ∞ \infty

其中,加粗的数字为本次松弛的距离。

ABCDE
queue01100

其中,加粗的进队的点。

3. 队列元素依次出队松弛相邻点,松弛点进队列

此时,点B出队,对点D进行松弛,而点D满足 d i s t [ B ] + w ( B , D ) = 4 < d i s t [ D ] = ∞ dist[B]+w(B,D)=4<dist[D]=\infty dist[B]+w(B,D)=4<dist[D]= 成立,则更新点D的距离向量,并让点D入队。

如法炮制,在队列中的元素依次出队,其松弛点入队,直到队列为空。

注意,即使是已经出队的点,也可能因为是另一个点的松弛点而再次入队。

最后得到的距离向量和队列如下。

ABCDE
dist03248

而队列则全为空。

ABCDE
queue00000
4. 判断是否存在负权回路

判断这一步可以在每个点进队或者出队的时候昨个记录,当某个点进队次数大于等于 n n n 的时候,说明存在负权回路。其实,就是每次该点都会更新到,而且每一次距离都小于上次。所以才会一直出队入队。

4. 总结分析

SPFA算法,个人的理解,队列的意义就在于避免像Bellman算法一样,每次都对每一条边进行松弛,这样复杂度确实很高。SPFA算法借用队列的形式,每次只对出队的点的相邻点进行松弛。这就大大减少了复杂度。

但是,SPFA算法稳定性较差,在稠密图中SPFA算法时间复杂度会退化。


算法对比表

内容FloydDijkstraBellmanSFPA
形式多源最短路径单源最短路径单源最短路径单源最短路径
算法动态规划贪心算法
复杂度 O ( N 3 ) O(N^3) O(N3) O ( N 2 ) O(N^2) O(N2)$O(V
边是否可以为负可以,但是不能是负权回路不可以可以,但是不能是负权回路可以,但是不能是负权回路

5. K条最短路径算法 (K Shortest Path Algorithms,KSP)

其实,这部分算法是基于上面的最短路径算法,只不过是选出来 k k k 条最短路径。这部分只介绍一种算法,Yen’s 算法。

参看文献:K条最短路径算法(KSP, k-shortest pathes):Yen’s Algorithm

1. 简介

K 最短路径(KSP)问题是最短路径问题的扩展和变形。1959 年,霍夫曼 (Hoffman)和帕夫雷(Pavley))在论文中第一次提出k 最短路径问题。通常,KSP问题分为两类,一类是有不含回路的路径限制,一类是没有任何路径限制。而Yen在1971年提出的Yen’s 算法只是针对第一类的问题,而且要求不存在负权重的边。其本质的思想是递推法中的偏离路径算法。

2. 问题描述

有6个点,C、D、E、F、G、H,两个点之间可能存在非负值的边。源节点为点C,目的节点为点H,求前3条从点C到点H的最短路径。(下图为动图,建议使用HTML文档查看)

在这里插入图片描述

3. 思路阐述

这里需要补充几个概念。

  • 偏移点:如下图所示,路径 P 3 P_3 P3 相对于路径 P 1 P_1 P1 的偏移点为点-2。偏移点可以理解为两条路径的分叉点。
  • 偏移边:如下图所示,路径 P 3 P_3 P3 相对于路径 P 1 P_1 P1 的偏移边为 ( 2 , 4 ) (2,4) (2,4)。偏移边可以理解为分出的路径的分叉边。
  • 偏移路径:如下图所示,路径 P 3 P_3 P3 相对于路径 P 1 P_1 P1 的偏移路径为 2 → 4 → 5 2\rightarrow 4\rightarrow 5 245。偏移路径可以理解从偏移点分出的路径。

在这里插入图片描述

1. 通过Dijkstra算法找出最短路径

通过Dijkstra算法,可以得到最短的路径 A 1 A_1 A1 C → E → F → H C \rightarrow E \rightarrow F \rightarrow H CEFH,其距离为 5 5 5

2. 将最短路径上的点逐个当做偏移点再次使用Dijkstra算法

这就是偏移路径算法的本质思想了。我们先将点C作为偏移点,此时,将点C和点E之间边的权重设为 ∞ \infty ,这样就可以避免与最短路径 A 1 A_1 A1 的重复了。

通过Dijkstra算法可以得到一个候选的最短路径 A 2 ( 1 ) A^{(1)}_2 A2(1) C → D → F → H C \rightarrow D \rightarrow F \rightarrow H CDFH,其距离为 8 8 8。这里为什么说是候选的最短路径呢?因为在将最短路径 A 1 A_1 A1 上的点逐一当做偏移点的过程中,可以产生一个偏移路径的集合 B B B,我们最后要在集合 B B B 中超出最短的路径当做第2条最短路径。所以,此时集合 B B B 中只有一条候选路径 A 2 ( 1 ) A^{(1)}_2 A2(1)

同理,我们依次将点E设为偏移点,将点E和点F之间的边权重设为 ∞ \infty ,可以得到候选的最短路径 A 2 ( 2 ) A^{(2)}_2 A2(2) C → E → G → H C \rightarrow E \rightarrow G \rightarrow H CEGH,其距离为 7 7 7,放入偏移路径集合 B B B 中。

接下来是点F设为偏移点,将点-F和点-H之间的边权重设为 ∞ \infty ,可以得到候选的最短路径 A 2 ( 3 ) A^{(3)}_2 A2(3) C → E → F → G → H C \rightarrow E \rightarrow F \rightarrow G \rightarrow H CEFGH,其距离为 8 8 8,放入偏移路径集合 B B B 中。

此时,偏移路径集合 B B B 中有三条候选的路径,分别是 A 2 ( 1 ) A^{(1)}_2 A2(1) A 2 ( 2 ) A^{(2)}_2 A2(2) A 2 ( 3 ) A^{(3)}_2 A2(3)。对比其距离,我们很容易发现,最短路径应该是为距离为 7 7 7 A 2 ( 2 ) A^{(2)}_2 A2(2),即 C → E → G → H C \rightarrow E \rightarrow G \rightarrow H CEGH。因此第二条最短路径为 C → E → G → H C \rightarrow E \rightarrow G \rightarrow H CEGH

3. 将第 i 条最短路径上的点逐一当做偏移点,重复步骤2,求出第 i+1 条最短路径

在求第三条最短路径的时候,我们将第二条最短路径按照步骤2的方式再求一遍就可以了。当出现距离一样的情况,我们选择最小节点数的路径就好。

最后我们得到第三条最短路径为 C → D → F → H C \rightarrow D \rightarrow F \rightarrow H CDFH,其距离为 8 8 8

4. 总结分析

KSP问题的解法有多种,这里只是介绍了一种Yen’s 算法。Yen’s 算法的本质是递归,然后逐一将最短路径上的点填上去,寻找新的最短路径。最巧妙的一点是将边权重值设为无穷,这样就可以直接寻找出新的路径了。


6. 最小生成树算法(Minimum Spanning Tree,MST)

参考文献:最小生成树的两种方法(Kruskal算法和Prim算法)

需知概念

  • 连通图:在无向图中,若任意两个顶点 v i v_i vi v j v_j vj都有路径相通,则称该无向图为连通图。
  • 强连通图:在有向图中,若任意两个顶点 v i v_i vi v j v_j vj都有路径相通,则称该有向图为强连通图。
  • 连通网:在连通图中,若图的边具有一定的意义,每一条边都对应着一个数,称为权;权代表着连接连个顶点的代价,称这种连通图叫做连通网。
  • 生成树:一个连通图的生成树是指一个连通子图,它含有图中全部 n n n个顶点,但只有足以构成一棵树的 n − 1 n-1 n1条边。一颗有 n n n个顶点的生成树有且仅有 n − 1 n-1 n1条边,如果生成树中再添加一条边,则必定成环。
  • 最小生成树:在连通网的所有生成树中,所有边的代价和最小的生成树,称为最小生成树。

在这里插入图片描述

求最小生成树的两种算法

Kruskal算法

Kruskal算法又称之为“加边法”,其核心思想就是迭代地将满足条件的边加入生成树中,直至没有满足的边存在为止。步骤如下:

  1. 将最小生成树边数初始化为0;
  2. 将图中所有边按照权重,从小到大依次排列;
  3. 将图中的 n n n个顶点分别看做独立的互不相连的由 n n n个树组成的森林;
  4. 按照权重从小到大选择边。如果被选中边的两个顶点不属于一片森林,则将这两个顶点的树合成一片森林,并将这条边作为最小生成树的一条边。如果两个顶点属于一片森林,则跳过。
  5. 重复步骤4,直至所有顶点的树都在一片森林中或者最小生成树存在了 n − 1 n-1 n1边。

算法流程图如下。

在这里插入图片描述

Prim算法

Prim算法与Kruskal算法不同的地方在与,Prim算法是“加点法“。通过向最小生成树点集合中,不断地加入新点,直至所有 n n n个点都被加入为止。步骤如下:

  1. 设置最小生成树点集合 u u u,并随机选取一点放入其中,例如将A点放入,即 u = { A } u=\{A\} u={A},则其余的点集为 v = { B , C , D , E , F } v=\{B,C,D,E,F\} v={B,C,D,E,F}。注意,这里首先放入的点并不一定是最小生成树的根节点。
  2. 在两个集合 u , v u,v u,v所能组成的所有边中,选择一条权重最小的边 ( u 0 , v o ) (u_0,v_o) (u0,vo),加入到最下生成树中,并将点 v 0 v_0 v0移入集合 u u u中。
  3. 重复步骤2,直到所有的点被移入集合 u u u中或者最小生成树中含有 n − 1 n-1 n1条边。

算法流程图如下:

在这里插入图片描述

Kruskal算法与Prim算法的对比表
Kruskal算法Prim算法
适用场景稠密图稀疏图
算法复杂度 O ( m l o g ( m ) + m α ( n ) ) O(mlog(m)+m\alpha(n)) O(mlog(m)+mα(n))
其中 n n n为顶点数量, m m m为边的数量, α ( n ) \alpha(n) α(n)为一次查询的复杂度
O ( ( n + m ) l o g ( m ) ) O((n+m)log(m)) O((n+m)log(m))
其中 n n n为顶点数量, m m m为边的数量
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值