求解最短路径常用算法详解、对比及实现(Dijkstra)(Floyd)(Bellman-ford)(SPFA)

求解最短路径常用算法总结(Dijkstra算法)(迪克斯特拉算法)(Floyd算法)(弗洛伊德算法)(Bellman-ford算法)(贝尔曼-福特算法)(SPFA算法)

相关算法实现

最短路径问题

最短路径问题是图论研究中的一个经典问题,目的在于寻找图中两个顶点之间的最短路径,在带权图(网)中寻找一个顶点到另一个顶点的最短路径,两个顶点之间带权路径长度最短的路径即为最短路径,在无权图中,路径长度最短的路径即为最短路。

四种算法的异同点

算法DijkstraDijkstra(堆优化)FloydBellman-fordSPFA
求解问题类型单源最短路单源最短路多源最短路单源最短路单源最短路
是否能够存在负权
是否能够判断负权环
空间复杂度O(V)1O(V)O(V2)O(V)O(V)
时间复杂度O(V2)O(V · lg(E)2O(V3)O(V·E)O(V·E)(最坏情况下)

Dijkstra算法

  1. 简介
    Dijkstra算法是经典的求解单源最短路径的算法,解决的是有权图中的最短路径问题,基于贪心思想。
  2. 数据结构
  • matrix矩阵:邻接矩阵,构建带权网,matrix[i][j]为顶点vI和顶点vj邻接边的边权,若两个顶点不存在边则为无穷大
  • dist数组:记录从源点到图中其他所有顶点当前的最短路径长度,初始化为无穷大,算法结束后可以得到源点到各个顶点最短距离长度
  • path数组:记录当前每个顶点在最短路径上的直接前驱顶点的序号,源点为其自身或无效值,算法结束后可以借助栈回溯得到源点到该顶点的最短路径
  • tag数组:标记顶点是否已经被访问且加入最短路序列,初始值为false
  1. 算法原理
  • 初始
    初始时,将源点加入最短路序列,tag置true,源点对应dist值置0,path置为自身
  • 选取顶点
    从未选取进入最短路径序列的顶点中选取距离已选入最短路径序列的任意顶点中最短的顶点vi,加入最短路径序列
    需要选取n-1次顶点进入最短路序列,即将源点以外的所有顶点都选入最短路径
  • 更新dist数组
    遍历新加入顶点vi的所有邻接顶点,判断是否存在从源点到达某个顶点的路径在经过顶点vi能够得到更短的距离,即if (dist[j] > dist[i] + matrix[i][j]),若存在则更新dist数组和path数组,即dist[j] = dist[i] + matrix[i][j]
  • 循环
    循环上述过程(选取和更新),n-1次后,考虑了经过除了源点以外的n-1个顶点的路径情况,即可得到路径即为源点其他所有顶点的最短路径
  1. 时空复杂度
  • 时间复杂度
    Dijkstra算法主要的时间开销在于选取顶点进入最短路径序列及其每次选取顶点后都需要对dist数组进行更新。图的存储结构为邻接矩阵时,算法时间复杂度为O(|V|2),图的存储结构为邻接表时,算法时间复杂度为O(|V||E|),|V|为顶点个数,|E|为边的个数。
  • 空间复杂度
    Dijkstra算法借助了dist数组、path数组和tag数组作为辅助空间变量,空间复杂度为O(|V|)
  1. 堆优化
    在边稀疏的图中可以考虑使用堆优化的Dijkstra算法,即在选取顶点时从最小堆中选取堆顶元素即可,选取最优顶点时间复杂度为O(1),而每次进行调整需要O(lg|V|)的时间消耗。故当图的存储结构为邻接矩阵时,时间复杂度为O(|V|·lg|V|),当图的存储结构为邻接表时,时间复杂度为O(|E|·lg|V|)
    在编程语言中可以调用库实现的堆(优先队列)。
  2. 贪心策略
    Dijkstra算法基于贪心的策略,每次选取当前距离最短的顶点加入最短路径序列,这是非常典型的贪心思想,经过|V|-1次选取和更新即可得到全局的最优解。这样的思想看似与动态规划类似,但不同的是贪心选取的是当前情况下的局部最优解,而贪心的局部最优解是假定的,之后可能会被更新掉(需要经过|V|-1循环),而动态规划得到局部最优解即为全局最优解的子结构。
  3. 带负权网
    Dijkstra算法不适用于带负权的图,由于Dijkstra算法是较“深”地进行寻路,一旦找到最短路径即加入最短路径序列,加入最短路径后无法再被更新,若出现负权降低距离消耗无法更新最短路径序列,Dijkstra算法更新的dist只能是与未加入最短路径序列的顶点的距离。
    在负权网中求解最短路径可以采用Bellman-ford算法
  4. 多源最短路径
    对n个顶点使用n次Dijkstra算法即可得到所有顶点之间的最单路径,时间复杂度为O(|V|3),堆优化的Dijkstra算法时间复杂度O(|V|· |E|·lg|V|)

Floyd算法

  1. 简介
    Floyd算法是经典的求解多源最短路径的算法,解决的是有权图中的多源最短路径问题,基于动态规划思想。
  2. 数据结构
  • matrix矩阵:邻接矩阵,构建带权网,matrix[i][j]为顶点vI和顶点vj邻接边的边权,若两个顶点不存在边则为无穷大,在算法中不断更新matrix矩阵,floyd算法结束后,得到的matrix矩阵即为最短路径矩阵
  • path数组:记录当前每个顶点在最短路径上的直接前驱顶点的序号,源点为其自身或无效值,算法结束后可以借助栈回溯得到源点到该顶点的最短路径
  1. 算法原理
  • 迭代
    从邻接矩阵(设为matrix(-1))开始,递归地进行n次更新,依次地得到matrix(0),matrix(1),······,matrix(n-1)其中matrix(k)[i][j]表示从顶点vi经过顶点序号小于k的所有顶点再到顶点vj的路径中长度最短的路径,经过n次迭代,考虑经过n个顶点的所有情况,即可得到多源最短路径结果,每次迭代后若出现更短路径则更新matrix矩阵对应值和path数组对应值,直到考虑所有情况
  • 动态转移方程
    matrix[i][j] = min { matrix[i][k] + matrix[k][j], matrix[i][j]}
  1. 时空复杂度
  • 时间复杂度
    Floyd算法的时间消耗主要在考虑每个顶点作为经过顶点的情况下从一个顶点到另一个顶点的路径长度,需要三个循环,当图的存储结构为邻接矩阵时,时间复杂度为O(|V|3),图的存储结构为邻接表时,时间复杂度为O(|E|·|V|2)
  • 空间复杂度
    Floyd算法借助了最短路径矩阵作为辅助空间变量,空间复杂度O(|V|2)
  1. 负权图
    Floyd算法可以在选择经过顶点的n次迭代操作中多次对matrix矩阵进行更新,类似于较“广”地寻路,故Floyd算法适用于带有负权边的图,但不适用于存在负权环的图,因为负权环可以无限降低路径长度(一直绕圈即可),可以说这样的图不存在最短路径,最短路径长度可以达到负无穷大。
  2. 传递闭包

Bellman-ford算法

  1. 简介
    Bellman-ford算法是求解单源最短路的一种算法,适用于带负权的图
  2. 数据结构
  • matrix矩阵:邻接矩阵,构建带权网,matrix[i][j]为顶点vI和顶点vj邻接边的边权,若两个顶点不存在边则为无穷大
  • dist数组:记录从源点到图中其他所有顶点当前的最短路径长度,初始化为无穷大,算法结束后可以得到源点到各个顶点最短距离长度
  • path数组:记录当前每个顶点在最短路径上的直接前驱顶点的序号,源点为其自身或无效值,算法结束后可以借助栈回溯得到源点到该顶点的最短路径
  1. 算法原理
  • 松弛
    松弛操作是对相邻节点的访问,对dist数组和path数组进行迭代,经过|V| - 1次松弛操作,考虑|V|-1个顶点为绕行顶点的情况后,即可得到最短路径
  • 判定负权环
    对每个顶点为绕行点的路径更新后,若网中不存在负权环则无法再缩减距离,即dist[i] + adjancentList[i][j].weight不变,若dist[i] + adjancentList[i][j].weight减少则表明存在负权环,由于adjancentList[i][j].weight保持不变,而dist[i]在负权环中被更新。故遍历所有的边,若第n次操作仍然可以降低开销,则说明存在负权环。
  • 核心代码
    matrix(k)[i][j] = min {matrix(k-1)[i][j], matrix(k-1)[i][k] + matrix(k-1)[k][j]}
  1. 时空复杂度
  • 时间复杂度
    Bellman-ford算法主要时间消耗在dist数组和path数组的更新中,两个循环,当图的存储结构为邻接矩阵时,时间复杂度O(|V|2),当图的存储结构为邻接表时,时间复杂度O(|E|·|V|)
  • 空间复杂度
    Bellman-ford算法借助dist数组和path数组,空间复杂度O(|V|)
  1. 与Dijkstra算法
    Bellman-ford算法与Dijkstra算法类似,dist数组不但迭代,直到(|V|-1)次比较后得到最短路径。但Dijkstra算法基于贪心策略,每一步都选取最好的情况,而Bellman-ford算法则只是简单地进行更新迭代。
  2. 负权图
    Dijkstra算法是较“深”地进行寻路,而Bellman-for算法则是较“广”地寻路,处理过的顶点对应的dist数组和path数组可以被更新,故能够适用于负权图,但与Floyd算法相同,图中不能带有负权环,负权环可以无限降低最短路径的长度,这样的图不存在最短路径长度。

SPFA算法

  1. 简介
    SPFA 算法是 Bellman-Ford算法的队列优化算法,适用于负权图求解最短路径
  2. 数据结构
  • matrix矩阵:邻接矩阵,构建带权网,matrix[i][j]为顶点vI和顶点vj邻接边的边权,若两个顶点不存在边则为无穷大
  • dist数组:记录从源点到图中其他所有顶点当前的最短路径长度,初始化为无穷大,算法结束后可以得到源点到各个顶点最短距离长度
  • path数组:记录当前每个顶点在最短路径上的直接前驱顶点的序号,源点为其自身或无效值,算法结束后可以借助栈回溯得到源点到该顶点的最短路径
  • q队列:用于暂存邻接顶点的队列
  • cnt数组:用于记录所有顶点入队的次数
  1. 算法原理
    借助队列存放待优化的顶点,每次将队首顶点的所有邻接点入队,更新邻接顶点最短路径累积权值,对每个邻接点进行“松弛”操作,若邻接顶点对应的dist数组值需要更新,则将邻接顶点入队,队首元素出队,重新选择队首元素,循环上述操作,直至队列为空。
    只有在当前顶点对应dist数组被更新的情况下,其邻接顶点的dist数组才可能需要被更新,若不变则无需入队,以降低时间复杂度
  2. 时空复杂度
  • 时间复杂度
    SPFA算法主要的时间消耗在dist数组和path数组更新过程,时间复杂度与需要更新dist数组的次数有关,最坏情况下时间复杂度为O(|V||E|)
  • 空间复杂度
    SPFA算法借助了dist数组、path数组、tag数组、q队列作为辅助空间,空间复杂度O(V)
  1. 负权环判断
    与Bellman-ford算法的思路相同,都是判断在|V|-1次迭代后最短路径,是否还会被更新。在SPFA算法中,每进入一次队列就需要进行一次更新,则若入队次数达到|V|,则表示存在负权环,在考虑所有情况后,仍可以降低距离消耗。

最后

  • 由于博主水平有限,不免有疏漏之处,欢迎读者随时批评指正,以免造成不必要的误解!

  1. V和|V|均表示图中的顶点个数 ↩︎

  2. E和|E|均表示图中边的数量 ↩︎

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值