最短路算法——Dijkstra

Dijkstra


在大多数最短路径问题中,Dijkstra 算法是最常用、效率最高的。它是一种“单源”最短路径算法,一次计算能得到从一个起点 s 到其他所有点的最短距离长度、最短路径的途径点。

一、Dijkstra的算法思想

Dijkstra 的模型例如多米诺骨牌,你可以想象下面的场景:

在图中所有的边上,排满多米诺骨牌,相当于把骨牌看成图的边。一条边上的多米诺骨牌数量,和边的权值(例如长度或费用)成正比。规定所有骨牌倒下的速度都是一样的。如果在一个结点上推倒骨牌,会导致这个结点上的所有骨牌都往后面倒下去。

        当我们在起点 s 推倒骨牌,可以观察到,从 s 开始,它连接的边上的骨牌都逐渐倒下,并到达所有能达到的结点。在某个结点 t ,可能先后从不同的线路倒骨牌过来;先倒过来的骨牌,其经过的路径,肯定就是从 s 到达 t 的最短路;后倒过来的骨牌,对确定结点 t 的最短路没有贡献,不用管它。从整体看,这是一个起点 s 扩散到整个图的过程。

而在这个过程中,观察所有结点的最短路径是这样得到的:

  1. 在 s 的所有直连邻居中,最近的邻居 u,骨牌首先到达。u 是第一个确定最短路径的结点。从 u 直连到 s 的路径肯定是最短的,因为如果 u 绕道别的结点到 s,必然更远。
  2. 把后面骨牌的倒下分成 2 部分,一部分是从 s 继续倒下到 s 的其它的直连邻居,另一部分从 u 出发倒下到 u 的直连邻居。那么下一个到达的结点 v,必然是 s 或者 u 的一个直连邻居。v 是第二个确定最短路径的结点。
  3. 继续以上步骤,在每一次迭代过程中,都能确定一个结点的最短路径。

我们用下面的表来总结 Dijkstra 算法的基本过程:

步骤 做法 具体操作 结果
1 从起点s出发,用BFS扩展它的邻居结点。 把这些邻居点放到一个集合A中,并记录这些点到s的距离。
2 选择距离s最近的那个邻居v,继续用BFS扩展v的邻居 (1)在A中找到距离s最小的点v,把v的邻居点,放到A中;
(2)如果v的邻居经过v中转,到s的距离更短,则更新这些邻居到s的距离;
(3)从集合A中移走v,后面不再处理v。
(1)得到了从s到v的最短路;
(2)v的邻居更新了到s的距离。
3 重复步骤2,直到所有点都扩展到并计算完毕 集合A为空。计算出了所有点到s的最短距离

        Dijkstra算法应用了贪心法的思想,即“抄近路走,肯定能找到最短路径”。算法可以简单概况为:Dijkstra = BFS + 贪心。实际上,“Dijkstra + 优先队列 = BFS + 优先队列(队列中的数据是从起点到当前点的距离)” 

        我们来分析一下Dijkstra 的复杂度:设图的点有 n 个,边有 m 条。编码的时候,集合 A 一般用优先队列来模拟。优先队列可以用堆或其他高效的数据结构实现,往优先队列中插入一个数、取出最小值的操作都是 O(logn) 的。一共往队列中插入 m 次(每条边都要进集合 A 一次),取出 n 次(每次从集合 A 中取出距离 s 最短的一个点,取出时要更新这个点的所有邻居到 s 的距离,设一个点平均有 k 个邻居),那么总复杂度是 O(m×logn+n×k×logn) ≈ O(m×logn),一般有 m 大于 n。不过要注意,在稠密图情况下 m 是 O(n^2) ,k 是 O(n)的。在计算单源最短路时,Dijkstra 是效率最高的算法。

       Dijkstra 存图使用的数据结构:题目若是稀疏图,往往 n 很大而 m 小,必须使用邻接表、链式前向星来存图;若是稠密图则 n 较小,就用简单的邻接矩阵,用邻接表也并不能减少存储空间。

        Dijkstra 的高效稳定: 从集合 AA 中得到一个点的最短路后,继续 BFS 时只需要扩展和更新这个点的邻居,范围很小,算法是高效的;而每次从集合A中都能得到一个点的最短路,算法是稳定的。 

        但Dijkstra的边的权值不能为负数。因为 Dijkstra 基于BFS,计算过程是从起点 s 逐步往外扩散的过程,每扩散一次就用贪心得到到一个点的最短路。扩散要求路径越来越长,如果遇到一个负权边,会导致路径变短,使扩散失效。见下图,设当前得到 s→u 的最短路,路径长度为 8,此时 s→u 的路径计算已经结束了。继续扩展 u 的邻居,若 u 到邻居 v 的边权是 -15,而 v 到 s 的距离为 20,那么 u 存在另一条途径 v 到 s 的路径,距离为 20 + (-15) = 5,这推翻了前面已经得到的长度 8 的最短路,破坏了 BFS 的扩散过程。

最短路算法——Dijkstra_第1张图片

二、Dijkstra的执行过程

        编程的主要内容是维护两个集合:已确定最短路径的结点集合 A 、这些结点向外扩散的邻居结点集合 B。程序逻辑是:

  1. 把起点 s 放到 A 中,把 s 所有的邻居放到 B 中。此时,邻居到 s 的距离就是直连距离。
  2. 从 B 中找出距离起点 s 最短的结点 u,放到 A 中。
  3. 把 u 所有的新邻居放到 B 中。显然,u 的每一条边都连接了一个邻居,每个新邻居都要加进去。其中 u 的一个新邻居 v,它到 s 的距离 dis(s,v) 等于 dis(s, u) + dis(u, v)。
  4. 重复步骤 2、3,直到 B 为空时,结束。

计算结束后,就可以得到从起点 s 到其它所有点的最短距离啦。 

举个栗子:如图,起点是 1,现在我们要求 1 到其它所有结点的最短路径。

最短路算法——Dijkstra_第2张图片

步骤如下:

  1. 1 到自己的距离最短,把 1 放到集合 A 里:A={1}。把 1 的邻居放到集合 B里:B={(2-5), (3-2)}。其中 (2-5) 表示结点 2 到起点的距离是 5 。
  2. 从 B 中找到离集合 A 最近的结点,是结点 3。在 A 中加上 3,现在 A={1, 3},也就是说得到了从 1 到 3 的最短距离;从 B 中拿走 (3-2),现在 B={(2-5)}。
  3. 对结点 3 的每条边,扩展它的新邻居,放到 B 中。3 的新邻居是 2 和 4,那么 B={(2-5), (2-4), (4,7)}。其中 (2-4) 是指新邻居 2 通过 3 到起点 1,距离是 4。由于 (2-4) 比 (2-5)更好,丢弃 (2-5),B={(2-4), (4-7)}。
  4. 重复步骤 2、3。从 B 中找到离起点最近的结点,是结点 2。在 A 中加上 2,并从 B 中拿走 (2-4);扩展 2 的邻居放到 B 中。现在 A={1, 3, 2},B={(4-7), (4-5)}。由于 (4-5) 比 (4-7)更好,丢弃 (4-7),B={(4-5)}。
  5. 从 B 中找到离起点最近的结点,是结点 4。在 A 中加上 4,并从 B 中拿走(4-5)。已经没有新邻居可以扩展。现在 A={1, 3, 2, 4},B 为空,结束。

我们在看看这个过程的复杂度:

        我们设图的边共有 m 个,需要往集合 B 中扩展 m 次。在每次扩展后,需要找集合 B 中距离起点最小的结点。集合 B 最多可能有 n 个结点。把问题抽象为:每次往集合 B 放一个数据;然后在 B 中的 n 个数中找最小值......

        这个最小值要怎么找呢?

如果往 B 中放数据是乱放,找最小值也是用类似冒泡的简单方法,复杂度是 n,那么总复杂度是 O(nm),和 Bellman-Ford 一样。不过上述方法可以改进,得到更好的复杂度,改进方法是:

  1. 每次往 B 中放新数据时,按从小到大的顺序放,用二分法的思路,复杂度是 O(logn),保证最小的数总在最前面;
  2. 找最小值,直接取 B 的第一个数,复杂度是 O(1)。

        这样 Dijkstra 算法总的复杂度就是 O(mlogn),是最高效的最短路算法。而我们在编程时,一般不用自己写上面的程序,直接用 STL 的优先队列就可完成数据的插入和提取。 

三、Dijkstra经典题目练习


 点击 -> Dijkstra经典题目分析及参考代码


王国

题目描述

小明是王国的王子,今天是他登基之日。在即将成为国王之前,老国王给他出了道题,他想要考验小明是否有能力管理国家。

题目的内容如下:

王国一共有 N 个建筑和 M 条单向道路,每条道路都连接着两个建筑,每个建筑都有自己编号,分别为 1∼N 。(其中皇宫的编号为 1)

国王想让小明回答从皇宫到每个建筑的最短路径是多少,但紧张的小明此时已经无法思考,请你编写程序帮助小明回答国王的考核。

输入描述

输入第一行包含三个正整数 N,M。

第 2 到 M + 1 行每行包含三个正整数 u,v,w,表示 uv 之间存在一条距离为 w 的路。

1 ≤ ≤ 3×10^5,1 ≤ ≤ 10^6,1 ≤ ui​,vi​ ≤ N,0 ≤ wi​ ≤ 10^9。

输出描述

输出仅一行,共 N 个数,分别表示从皇宫到编号为 1∼N 建筑的最短距离,两两之间用空格隔开。(如果无法到达则输出 -1)

样例输入

3 3 
1 2 1
1 3 5
2 3 2

样例输出

0 1 3

如有错误和需要改进完善之处,欢迎大家纠正指教。

你可能感兴趣的:(算法,c++,c语言,leetcode,算法,最短路径)