dijkstra算法练习:https://www.geeksforgeeks.org/dijkstras-shortest-path-algorithm-greedy-algo-7/
详见8月6号的jupyter notebook
然后是dijkstra合理性的证明,使用反证法,详见ipad.
以下介绍性的内容出自知乎专栏:https://zhuanlan.zhihu.com/p/129373740
Dijkstra 算法,是由荷兰计算机科学家 Edsger Wybe Dijkstra 在1956年发现的算法,戴克斯特拉算法使用类似广度优先搜索的方法解决赋权图的单源最短路径问题。Dijkstra 算法原始版本仅适用于找到两个顶点之间的最短路径,后来更常见的变体固定了一个顶点作为源结点然后找到该顶点到图中所有其它结点的最短路径,产生一个最短路径树。本算法每次取出未访问结点中距离最小的,用该结点更新其他结点的距离。需要注意的是绝大多数的Dijkstra 算法不能有效处理带有负权边的图。
下面,我们就从一个赋权的有向图为例开始解释Dijkstra 算法。
设一个赋权有向图 。其中的每条边 的权值为一个非负的实数 ,该权值表示从顶点 到顶点 的距离。并设一单源点 。现在我们的任务是:找出从源点 出发,到 中所有的节点的最短路径。
我们来看一个具体的例子:
这是一个具有 个顶点的赋权有向图,其顶点集合为 ,其权值分别为:
现在我们选定 为原点 :
则从源点 出发,到 中所有顶点的最短路径分别为:
即:
其中, 表示从源点 出发,到 中的顶点 的最短路径。
注:最短路径可以理解为所有可能的路径中总权和最小的那一条路径。举一个再简单不过的例子:你开车从城市 到城市 ,假设有很多条路可以走,最短的那条路就是最短路径,总权和可以理解为总的公里数。
以上是我们通过观察和计算比对出来的最短路径,下面我们就来看看Dijkstra 算法是如何帮助我们找到这些所有的最短路径的。
在开始之前,有几个概念需要明确一下。
- 定义一个集合 ,如果集合 中的某个顶点 在集合 中了,那么就说明从源点 到顶点 的最短路径已经被找到,而在算法一开始的时候,集合 中只有源点 。即:
而且,当且仅当 的时候算法执行完毕。此时顶点集 中的所有元素都被放进了集合 种,也就是说除了源点以外的所有从源点出发到其余所有顶点的最短路径已被找到。
注:当然了,你也可以认为源点到自己本身的最短路径也被找到了。对于任意一个无自环的源点,它到自己本身的最短路径都是 。
- 下面这个概念可能稍微有些抽象,不过没有关系,这里理解不了的话我们一会讲例子的时候会进行具体说明。这个概念叫做从源点 到顶点 (一开始 )的相对于集合 的最短路径。 即从源点 到顶点 的路径中间只能经过已经包含在集合 中的顶点,而不能经过其余的还未在集合 中的顶点。而这个相对于集合 的最短路径的长度我们记作:
而我们之前的 表示的是全局的从源点 到顶点 的最短路径,这个最短路径没有限制“必须在路径中间只能经过已经包含在集合 中的顶点”,这个全局的最短路径才是我们要的最终解。所以,一般有关系:
而我们的Dijkstra 算法要做的就是通过不断计算 进而不断的扩充集合 ,当集合 不断被扩充的时候,相对于集合 的最短路径会越来越短,直到 入集合 之时,此时我们便得到了 ,且此时有 。下面我们来看看算法的设计思想:
输入:赋权有向图 。输出:从源点 到所有的 的最短路径。 初始 ; 对于 ,计算 ; 选择 ,并将这个 放进集合 中,更新 中的顶点的 值; 重复 ,直到 。
然后是Dijkstra 算法的伪码:
下面我们来解释一下这个伪码:
算法初始,将选择的源点 放进集合 中;
无自环的源点 到自己的最短路径为 ;
当顶点 不在集合 中时(此时集合 中仍只有源点 ),开始进入循环;
将源点 与点 之间的权值赋给 。由于是有向图,所以当源点 不指向任何其他集合 外的顶点时, 。可以理解为此时从源点 出发,暂时是达到不了 的。不过后来随着集合 的扩充,从源点 出发一定能到达所有的顶点。一会我们讲解例子时会出现这种情况。此时第一个 循环结束。
如果集合 不是空集,则进入循环;
选出经过第一个 循环之后的,在集合 中的,且相对于集合 的最短路径中距离最短的那个顶点 ;
将这个顶点 并入集合 ,从而达到扩充集合 的目的;
将顶点 并入集合 之后可能会对其他顶点相对于集合 的最短路的长度会有影响,所以进入内 循环对有影响的进行更新;
即如果从源点 到我们在第 步选出的顶点 的相对于集合 的最短路径的长度再加上顶点 到顶点 之间的距离 还要小于源点 到顶点 的相对于集合 的最短路径的长度还要短的话;
则将源点 到顶点 的相对于集合 的最短路径更新成源点 到我们在第 步选出的顶点 的相对于集合 的最短路径再加上顶点 到顶点 之间的权值 。
下面我们开始讲例子,我们还是以图片1中的赋权有向图进行说明。
首先我们还是选择 为原点 ,那么在算法的开始, 。之后我们计算除了 以外的其余顶点到 的距离 ,即寻找所有的除了 以外的所有顶点相对于集合 的最短路,即从 出发,到达所有顶点且只允许通过顶点 (因为此时集合 中只有 这一个元素)的最短路径。这是我们的算法中的第一个 循环在做的事情。这时候我们发现想要只通过顶点 而到达顶点 都是不可能的,所以我们有:
而 就是算法中所说的暂时到达不了的顶点了。现在算法的前四步已经结束了,现在开始第五步检验集合 是否是空集,这里显然不是,这里:
现在进行第六步。第六步是选出经过第一个 循环之后的,在集合 中的,且相对于集合 的最短路径中距离最短的那个顶点 。那我们看看在式 中那个顶点距离源点 最短就好了,显然是 ,所以,我们这里选择的 。
那么第七步就是将 放进集合 中了。此时集合 。这就是说明从源点 出发,到顶点 的最短路径已经被找到了。
下面我用绿色表示被放入集合 中的顶点:
的颜色我就不变了,因为它一直都在集合 中。此时:
这就说明下次在找相对于集合 的最短路径的时候 中就有两个点可以被通过了,这样也许就会使得一些原来到达不了的顶点由于可以多经过一个点而到达,这也就是算法中所说的当我将一个新的顶点并入集合 之后,其他的在集合 以外的顶点的相对于集合 的最短路径的长度可能会发生改变,因为有些原来暂时到达不了的顶点现在可以到达了。具体的来讲,我们有:
这个更新步骤我也来详细是说一下,这是算法第八到第十步所做的事情。比如 ,一开始在集合 中只有源点 ,而找到 相对于集合 的最短路径只能通过顶点 ,这样我们在式 中所得到 。但是当顶点 也进入到集合 之后我们再找 相对于集合 的最短路径时就可以先通过顶点 然后到顶点 ,最后再到 。现在这两种走法都可以,但是算法究竟选择哪种算法还是要判断哪种走法距离最短,即比较 :
之间的大小关系,谁小算法就选择谁。经过比较发现:
所以选择后者。再比如原来达到不了的 ,现在由于集合 中多了顶点 变得可以达到了,即:
所以算法肯定选择后者。不过此时算法也没得可选,先要到达顶点 就必须走这条路。
其余发生变化的顶点分析类似,大家可以自己试试。
现在算法从头到尾被执行了一遍了,然后我们回到第五步判断 循环的条件还是否为真,此时:
所以再执行 循环,由第六步从式 中选择出属于集合 ,且相对于集合 的最短路径中距离最短的那个顶点为 ,所以,这里我们选择的 。然后第七步 将 放进集合 。此时,集合 :
此时我们有:
可见这次没有发生更新,且此时的:
所以再执行 循环,由第六步从式 中选择出属于集合 ,且相对于集合 的最短路径中距离最短的那个顶点为 ,所以,这里我们选择的 。然后第七步 将 放进集合 。此时,集合 :
此时我们有:
可见这次在顶点 处发生了更新(至于为什么 大家可以自己分析一下试试),且此时的:
所以再执行 循环,由第六步从式 中选择出属于集合 ,且相对于集合 的最短路径中距离最短的那个顶点为 ,所以,这里我们选择的 。然后第七步 将 放进集合 。此时,集合 :
此时我们有:
可见这次并未发生更新,且此时的:
所以再执行 循环,由第六步从式 中选择出属于集合 ,且相对于集合 的最短路径中距离最短的那个顶点为 ,所以,这里我们选择的 (至剩下 可以被选择了)。然后第七步 将 放进集合 。此时,集合 :
此时我们有:
这是最后一次了,且这次并未发生更新,且此时的:
则不满足算法中的 循环的条件,循环结束,算法结束。显然,此时有 。
最后我们来看一看Dijkstra 算法的时间复杂度。
Dijkstra 算法的时间复杂度是 。其中:
分别为赋权有向图中的顶点个数和边的个数。Dijkstra 算法的时间复杂度是 是因为算法总共进行 步,每一步选出一个具有最小 值的顶点放入集合 中,需要 的时间。
而选择基于堆实现的优先队列数据结构,可将Dijkstra 算法的时间复杂度降为 。
参考:
屈婉玲教授——《算法》课程。