简介
Dijkstra's algorithm是一个求图中单点到其他所有点最短距离的算法。我在之前的一篇文章里也有过一些讨论。只是那篇文章写得比较仓促,对于该算法的思想和推导理解得也并不深刻。经过一些时间的思考,这里想对该算法的思想做一个进一步的阐述,并对一些和它相关的问题进行比较讨论。
算法描述分析
对于这个算法来说,它有这么一个前提。在一个连通的图中(可以是有向图或者无向图),所有的边都有一个对应的权值。我们给定一个起点,需要求基于这个起点到所有其他节点的最短路径。为了解决这个问题,Dijkstra's algorithm的解决步骤如下:
1.设定给定源节点到自身的边权值为0,而到所有其他节点的权值为无穷大。
2.找到距离起点最短的边,因此找到对应的一个节点。这个节点是从起点开始能找到的最近的点。
3.查找是否有到这个最近点的邻接点更短的路径,如果有,则更新这些路径值。
4.重复上述步骤2, 3直到涵盖所有节点。
在讨论这个算法的具体正确性以及复杂度之前,我们先以下图为例用上述的步骤来推导一下。
假定图中的红色节点是我们指定的起点。同时我们用一个数组来表示从源节点到其他节点的最短距离。那么,我们按照前面的步骤来说,对于红色节点本身来说,它最短的边就是到它自身的,而且长度为0。这个时候,我们所知的边如下表:
对应的这个最短长度数组如下:[0, INFINIT,
INFINIT, INFINIT, INFINIT, INFINIT, INFINIT]
按照前面的过程,我们取权值最小的边。这里最小的就是源节点本身,权值为0。然后,我们要遍历它所有邻接节点来判断是否有更短的路径。在这里,因为最开始初始化为它到其他节点的距离都是无穷大。于是我们相邻两个节点的距离实际上更近,需要更新节点1和2。于是我们得到如下图:
这个时候我们得到的边如下:
边 |
权值 |
0 --> 1 |
4 |
0 --> 2 |
10 |
对应的最短路径数组更新为:[0, 4, 10, INFINIT, INFINIT, INFINIT, INFINIT]。我们要注意到一点,每次取我们表里面最短的边之后,就将它从表里面移除。
我们再取里面里面最短的边0 -- > 1。 通过节点1再去比较更新它的邻接节点。此时,节点4的值需要被更新。于是有下图:
这个时候,对应的边如下:
边 |
权值 |
0 --> 4 |
25 |
0 --> 2 |
10 |
对应的最短路径数组为[0, 4, 10, INFINIT, 25, INFINIT, INFINIT]。我们再去表里面最小权值的边0 --> 2,并比较节点2的所有邻接节点:
对应更新后的表如下:
边 |
权值 |
0 --> 4 |
25 |
0 --> 3 |
15 |
0 --> 5 |
18 |
对应的最短路径数组为[0, 4, 10, 15, 25, 18, INFINIT]。我们再取里面权值最短的边0 --> 3:
对应的表如下:
边 |
权值 |
0 --> 4 |
20 |
0 --> 5 |
18 |
这一步比较有意思,我们通过节点3的时候又访问到节点4了,之前我们计算出来它的最短路径是25,而通过节点3得到的最短路径是20。于是我们要更新最小路径数组里对应的值,得到对应的数组为[0, 4, 10, 15, 20, 18, INFINIT]。这个时候,我们再取表里面最短的路径0 --> 5:
在节点5这一步,它又可以遍历到节点4,而按照它的距离加上到节点4的边的权值,这个距离值是30,明显大于前面我们计算出来的20。所以,这里不会更新最短路径数组。这样,我们得到的表如下:
而这里最短路径数组还是和原来的一样:[0, 4, 10, 15, 20, 18, INFINIT]。现在,我们再取最后的边0 --> 4:
我们可以得到对应的表如下:
最短路径数组是[0, 4, 10, 15, 20, 18, 24]。我们再取边0 --> 6的时候已经找不到更短的可以更新的节点了。这样,我们就按照这个过程遍历更新了所有的节点。
要点
在上述算法的演示过程中,我们用一个表来保存相对于源节点来说它当前能到达的节点的距离。每次取里面最短的那个边,然后再去根据这个边对应的点去检查更新它的邻接节点。而每次在取走最短路径的边并更新到邻接节点距离的值的过程相当于将当前最短路径的这个点给抹去了。
在这一步,我们可能会有几个疑问。一个是为什么我们每次要取权值最小的边?而且,我们每次取这个最小权值的边为什么是正确的呢?这是怎么保证的?我们可以以前面示例中的一些过程来说明。以最开始选择0节点的两个邻接节点为例:
源节点到1和2的距离分别为4和10。因为它是从节点0直接相连的,我们取0 --> 1作为当前的最短路径,也就确定了0 --> 1的路径是全局里从节点0到节点1的最短路径,而不会有经过其他点到达节点1而形成更近的节点了。为什么会这样呢?因为我们当前选取的边已经是最小的了,没有比它更小的,所以就算我们选择到其他的边以后也可以连接到节点1,但是其他的边本身已经比边0 -- >1还要大,再加上其他的边连接,只会更大。所以我们就保证了当前取的这个最短边已经是全局最优了。
当然,在这里只是针对源节点连接的第一步,对于后续的节点呢?它们是否也符合这样的情况?按照前面的讨论。我们找到一个当前最短路径时,比如0 -- >1,我们就更新1的邻接节点,于是就有了0 -- > 4。这时计算出来的值是25。这个过程我们就可以假想为我们将节点1给消除了,只有一个从节点0直接连接到4而且长度为25的边。而这个时候,我们再找到的最短路径就已经是0 -- >2了。它本身也相当于我们前面讨论的情况。针对源节点直接连接的点,最短的边也是全局范围内两个点之间最短的路径了。就这样继续不断的推演...
因此,对于Dijkstra算法,这里最核心的问题就是我们每次取出来的最短路径就是一个全局最优。我们可以将每次取出最短路径并更新邻接节点的过程看成是加入新的直连节点的过程。这样,我们就更容易理解这个问题了。
前提条件
我们前面的推导和讨论其实是基于两个条件的:
1. 图是连通的
2. 所有边的权值是正数。
对于条件1来说,如果图不是连通的,则我们从源节点到很多节点的边权值可以说是无穷大,这个时候就失去比较的意义了。而对于条件2,我们前面的推导里有提到。我们取的当前最小权值边是全局最优的。因为其他比这个边大的边再走若干步的路径到对应的节点会更大。而使得这个距离更大就是因为每一步的权值都是正数,所以我们可以每次放心的把原来的最小节点给删除。而如果出现权值为负数的情况,则还是可能出现从一个权值较大的边最终走到目的节点反而总权值更小。
数据结构的选取
根据问题的要求,最核心的问题就是我们需要用一个结构来表示图。我们可以用典型的邻接表方式来表示。当然,在这个问题里因为要有每个边之间的权值。我们可以在每个邻接表的元素里保存一系列对应的边。实现的时候可以定义一个专门的WeightedEdge对象。也可以用一个简单的Key value pair来表示。一个表示对应节点的编号,一个表示对应的权值。
因为要记录和更新到每个节点的距离值,我们可以用一个数组来记录它们,比如前面示例分析里用的dist[]数组。如果我们要记录最短路径节点,也可以用一个edgeTo[]数组来表示。每个索引位置的元素记录当前节点的前一个节点。
当然,还有一个比较重要的地方就是每次遍历更新的边信息。因为希望每次取权值最小的边。从这一点来说看起来我们可以用一个最小堆。但是因为在比较的过程中可能还需要更新表里面的值。所以有的实现是使用一个改进后的indexMinHeap。相当于对最小堆里的元素建了个索引。这种方式相对比较复杂,但是效率比较高。只是目前来说没有现成的类库支持。还有一种办法就是利用一个优先队列结合一个数组来实现。我在之前文章里也有过讨论。
和prim算法的差别
如果我们仔细看Dijkstra's algorithm会发现它和prim算法很像。因为它们都是根据一个给定的节点,然后扩展到整个图。但是如果仔细去看它们扩展的过程,我们会发现一些细微的差别。Dijkstra算法是计算到给定点最短距离的边,所以每次往外面扩展的时候是取当前最小权值的边再往表里面增加并更新原来的结果。而prim算法是计算到目前涵盖集合里最短的边,所以它不需要是到最开始源节点最短的距离,只要是到当前涵盖的节点集合里元素最近的那个就可以。所以每次它碰到的边不需要做任何加权值的运算而是直接加入到堆里,回头取最小的来判断处理就可以了。
总结
这篇文章算是对Dijkstra算法的一个思路重新推导。希望能够从一个更加直观的角度来理解整个过程。它本质上是利用了一个从某个节点连接的边里取到的最短的边就一定是全局来说最优的两点路径。所以每次在找到最短路径并更新邻接节点的时候,我们可以将它想象成去掉原来最短路径的点并形成新的更长的边的过程。
Dijkstra's algorithm只适用于边的权值为正数的场景,对于权值为负数的场景,我们应该考虑Bellman-Ford算法。
参考材料
Grokking Algorithms
Algorithms
相关推荐
**Dijkstra's算法详解** ...总结起来,Dijkstra's算法是解决单源最短路径问题的关键工具,其背后的原理和实现对于理解图论和算法设计至关重要。结合提供的文件,我们可以深入学习和实践这个经典的算法。
**Dijkstra算法详解** Dijkstra算法,由荷兰计算机科学家艾兹格·迪科斯彻(Edsger W. Dijkstra)提出,是一种用于...通过对`dijkstra.m`文件的学习和理解,我们可以更好地掌握这一经典算法,并将其应用于实际问题中。
迪杰斯特拉(Dijkstra's)算法是一种在加权图中寻找最短路径的经典算法,由荷兰计算机科学家艾兹格·迪杰斯特拉在1956年提出。这个算法适用于寻找单源最短路径,即从图中的一个特定起点到其他所有节点的最短路径。在...
**迪杰斯特拉算法(Dijkstra's Algorithm)** 迪杰斯特拉算法是由荷兰计算机科学家艾兹格·迪杰斯特拉在1956年提出的一种单源最短路径算法。这个算法主要用于解决图论中的最短路径问题,尤其适用于加权有向图。它通过...
**文件"Dijkstra-s-algorithm-main"**可能包含了完整的Dijkstra算法Java实现代码,包括顶点类、图类、优先队列实现以及主程序。通过分析和运行这个代码,你可以深入理解Dijkstra算法的工作原理,并学习如何在实际...
理解并能熟练运用Dijkstra算法是任何IT专业人员,尤其是从事图形算法、网络工程或软件开发的人员的基本技能之一。通过这个MATLAB程序,你可以更好地理解和实践这个算法,从而提高你的问题解决能力。
对于这类情况,可以考虑使用Bellman-Ford算法或Johnson's algorithm。 总之,Dijkstra算法在Python 3中的实现使得我们能够高效地解决许多实际问题,通过理解其原理和应用,我们可以更好地利用这一工具来优化路径...
在"Dijkstra-s-Shortest-Path-Algorithm-master"这个压缩包文件中,很可能包含了以下内容: - 源代码文件:Java程序实现Dijkstra算法,可能包括一个`Graph`类用于表示图,以及一个`Dijkstra`类包含实际的算法逻辑。...
在`Dijkstras.algorithm-master`这个压缩包中,你将找到这些类的实现,以及可能的测试案例,帮助你理解Dijkstra算法在Java中的具体应用。通过阅读和学习这些代码,你可以深入理解算法的工作原理,以及如何在实际编程...
此时,可以使用其他算法,如Bellman-Ford算法或Johnson's algorithm。 总结来说,Dijkstra算法是解决图论中最短路径问题的利器,通过贪心策略逐步找到从起点到各个节点的最短路径。理解并熟练掌握Dijkstra算法对于...
这时可以考虑使用Bellman-Ford算法或Johnson's algorithm。 - 对于大规模图,可以使用A*搜索算法,结合启发式函数来减少搜索范围,提高效率。 综上所述,Dijkstra算法是解决带权有向图中最短路径问题的经典方法,在...
这可以通过Dijkstra算法、A*搜索算法或者结合Bresenham’s line algorithm来实现。Bresenham’s algorithm在这里可以用于从当前节点到目标节点的局部路径规划,因为它能快速生成直线路径。 实现过程中,MATLAB代码...
同时,为了处理带有负权边的图,可能会采用其他算法,如Bellman-Ford算法或Johnson's algorithm。 总的来说,Dijkstra算法是图论和算法设计的基础内容,掌握其原理和实现对于任何IT从业者来说都是非常重要的。通过...
在这种情况下,可以使用其他算法,如Bellman-Ford算法或Johnson's algorithm。 总的来说,Dijkstra算法的可视化实现是学习和理解最短路径问题的一个强大工具。它不仅帮助编程初学者掌握算法的逻辑,还使非专业人士...
此外,字符串处理也是PHP中常见的应用场景,如KMP算法、Rabin-Karp算法用于字符串匹配,以及Manacher's Algorithm解决回文子串问题。这些算法的PHP实现,不仅有助于我们掌握字符串处理技巧,还能启发我们在实际项目...
- 对于大规模图,可以考虑使用贪心算法的变种,如Floyd-Warshall或Johnson's algorithm,它们能处理所有顶点对的最短路径。 Dijkstra算法是图论中基础且重要的算法之一,理解并掌握其原理和实现对于解决实际问题...
包括排序算法(如冒泡排序、快速排序、归并排序)、搜索算法(如二分查找、广度优先搜索、深度优先搜索)、图论算法(如最短路径算法Dijkstra、最小生成树Prim和Kruskal)、动态规划(如背包问题、最长公共子序列)...
常见算法包括深度优先搜索(DFS)、广度优先搜索(BFS)、最小生成树算法(Kruskal's、Prim's)、最短路径算法(Dijkstra's、Floyd-Warshall)等。 通过研究"Programming-master"中的代码实现,我们可以直观地看到这些算法...
分治策略则将大问题拆分为小问题求解,如Strassen矩阵乘法、 Kadane's algorithm等。 9. **C++编程语言特性**:Sedgewick的实现使用了C++,所以学习过程中也会涉及C++的面向对象编程、模板、STL库(容器、迭代器、...
Kruskal's和Prim's算法则用于最小生成树问题,这些都是解决实际问题的利器。动态规划,如斐波那契序列、背包问题和最长公共子序列,它通过将问题分解为子问题来寻找最优解,是解决复杂优化问题的强有力工具。 在...