参考资料:
《数据结构》 严蔚敏
《算法导论》
《算法导论》
据说这个求单源最短路径的算法是Dijkstra爷爷1956年为了展示新计算机ARMAC的计算能力,初试身手的成果,属于他的算法处女作。据Dijkstra爷爷自述,他搞出最短路径算法的时候连纸笔都没用。当时他和他老婆在阿姆斯特丹一家咖啡厅的阳台上晒太阳喝咖啡,突然就把这个算法想出来了...听到这个传说我深受打击,因为我是花了数个小时时间苦苦思索,快要把脑袋想破的时候,才想明白这个算法是怎么回事...
本文不讨论实现的细节,只是试图“比较形象地”讲明白算法中某些不易理解的关键步骤。
给定一个带权有向图G=(V,E)和源点V0,求从V0到G中其余各顶点的最短路径。
首先约定,图G中共有n个顶点。
该算法设置了一个dist[n]数组,一个arc[n][n]数组,和一个集合S,下面分别解释这三个量的意义。
S:若一个顶点 vk 被纳入S,则说明从v0到vk的最短距离已被确定,该最短距离即为dist[k]
dist[i]:从v0出发,只经过那些在S中的顶点而到达vi的最短路径长度。若vi已被纳入S,则此时的dist[i]就已经是最终找到的从v0到vi的最短距离,若vi尚未被纳入S,则此时的dist[i]并非最终最短距离,尚需在以后的每一步都看看dist[i]是否能被缩的更短。arc[i][j]:这个二维数组实际上是图G的邻接矩阵,arc[i][j]的值代表了从vi到vj的直接距离,即边<vi,vj>的权值,若从vi到vj没有边,则arc[i][j]被设置为∞初始状态:
初始时S中仅有v0dist[i]的初始状态:若从v0到vi有弧,则dist[i]为该弧上的权值,否则dist[i]为∞
第一步
①先从数组dist[n]中找到一个值最小的,即找到一个j使得dist[j]=min{dist[i] | i∈V},则易知(v0,vj),就是从v0到vj的最短路径。
证明:若有其他路径存在,比如有(v0,vk,vj)存在,则 ∵ dist[j]=min{dist[i] | i∈V},∴dist[k]≥dist[j] ∴路径(v0,vk,vj) 一定比路径(v0,vj)长②既然从v0到vj的最短路径已被确定,即为dist[j],于是就把vj加入S中③审视dist[i]的意义:“从v0出发,只经过那些在S中的顶点而到达vi的最短路径长度”由于集合S在步骤②时被加入了新的元素vj,于是对应的dist[i]也可能因此发生变化。此时S中有两个素v0和vk,我们要检查每一个vi∉S,若dist[j]+arc[j][i] < dist[i], 则意味着(v0,vj,vi)这条路比(v0,vi)更短,于是将dist[i]的值改为dist[j]+arc[j][i]经过步骤③之后,在S={v0,vj}的情况下,dist[i]仍很好地保持了其原有的意义,即“从v0出发,只经过那些在S中的顶点而到达vi的最短路径长度”
然后,重复上述步骤,直至所有顶点都已被纳入S
①每一次都从那些vi∉S的顶点中选出对应的dist[i]最小的那个顶点,即找到一个dist[j]=min{dist[i] | i∈V},可知这个dist[j]就是从v0到vj的最短路径长度,证明如同第一步中的①。②将vj纳入S③更新dist[i],这是关键步骤。为方便叙述,我们设集合Li={从v0出发只经过S中的元素而到达vi的所有路径}。由于vj被新加入了S,使得Li也相应扩大了。现可将Li分成两部分,一部分为 LiWithVj={Li ∩ 包含vj的路径},另一部分为LiWithOutVj={Li ∩ 不包含vj的路径},其中LiWithVj是由于vj新加入S而使得Li在原有基础上扩大的部分。
为了更新dist[i],我们只需找出LiWithVj中的最短路径p1和LiWithOutVj中的最短路径p2,然后比较p1和p2的长度,将较小的长度写入dist[i]即可。很显然p2的长度即为未更新前的dist[i],所以我们只需找到p1。现在来找p1, 请看图2。
易知“从v0出发,经过vj而到达vi的最短路径”可被分为两部分,即“v0到vj的最短路径”+ “vj到vi的最短路径”,前半部分已被确定了,即为图中蓝线部分,其长度为dist[j]。所以只需要找到后半部分,即找“vj到vi的最短路径”,此时要注意,整个讨论都基于“只经过那些在S中的顶点”,所以问题应表述为“寻找从vj出发只经过那些属于S的顶点而到达vi的所有路径中的最短的一条”。
显然,当(vj,vi)为vj到vi的最短路径时,p1应该取为(vj,vi),即不经过任何中间顶点由vj直达vi的路径。若(vj,vi)并非最短路径,假设存在某个vk使得 length(vj,vk,vi) < length(vj,vi),如图2中 length3 + length2 < length1 ,此时依然应将p1取为(vj,vi),也就是说,不管什么情况,都应该直接取p1为 dist[j]+arc[j][i],然后拿p1和p2做比较,将较小值写入dist[i]即可。
证明:
如图2,假设存在vk使得 length(vj,vk,vi) < length(vj,vi),即length3 + length2 < length1则 length2 < length1又∵ dist[k] ≤ dist[j] (因为vj比vk加入S的时间要晚)∴ dist[k]+length2 < dist[j]+length1而 dist[i] ≤ dist[k]+length2 (由dist[]数组的定义知,dist[i]是“由v0出发只经S中元素(不含vj)而到达vi的最短路径的长度”即图中绿线长度,而dist[k]+length2是“由v0出发只经S中元素(不含vj)而到达vi”的众多路径中的一条)∴ dist[i] < dist[j] + length3 + length2综上: 若存在vk使得length(vj,vk,vi) < length(vj,vi)成立,则p1注定要在下一步与p2的比较中被淘汰,因为即使将p1取为dist[j]+length3+length2,在下一步p1与p2(即[dist[i]])比较时也一定有dist[i] < dist[j] + length3 + length2,这个p1将不会派上用场。
所以我们得到了更新dist[i]的方法:为了程序的规整,每次向S中加入一个新的顶点vj时,对于每个尚不属于S的vi,都比较dist[i]和dist[j]+arc[j][i]之大小,将较小的值写入dist[i]中。