本文图片及算法均来自 《算法导论》,建议阅读英文原版,里面有更加详细的介绍和算法证明。
在进入Dijkstra算法之前先把书中一些关于图的表达表示清楚,会更加的清晰。因为本质上Dijkstra算法是一个基于图的算法,如果不把一些图的基本概念搞清楚可能会出现难于理解或者理解的偏差。所以这里还是觉得,有能力一定要自己去看英文的原版,尽可能的减少翻译或者别人解释所带来的的理解偏差。
图G的基本表示,G = (V,E)。 V(vertex)为图里所有顶点,E(edge)为图里所有的边,w(U,V)为从顶点U到顶点V的权重,可以简单的理解为距离(如果是想找一条最短距离的话),如果要更详细一些的话,一个带权重的图可以表示为G = (V,E,w),w为所有边的权重的集合。
图的表示有两种,如下图所示,这里的所有算法都是基于图是用Adjacency list来表示的(下图b中所示)。比如,G.adj [1] = {2,5}, 说明在图G中,顶点1和顶点2,5相连。
Dijkstra’s algorithm:
Given: A weighted, directed graph G(V,E) with edge weights w, w stores weight w(u,v) for each edge (u,v) belongs to E. Assume w(u,v) >= 0.
输入:一个有向权重图(无向图也可以),并且各权重大于等于0。起点用source s表示。
返回:?
这里返回的其实是两个数组,一个数组表示起始点source s到各个顶点的最短距离,另一个数组表示在最短路径中,各顶点的前任顶点(predecessor)也可以理解为父顶点,比如我们有一条最短路径从s到b,s -> a -> b, 那么数组里b对应的父顶点就为a。
第一个数组很好理解,我们要求最短的路径嘛,从s到各个顶点的距离直接数组里取出来就行了。
第二个数组是干嘛用的呢?为什么要记录顶点的父顶点呢?很自然,我们不止想要知道最短路径的距离,我们还想知道路径是怎么来的,这时候,我们只要从终点开始,顺着终点的父顶点,一路回溯到起点就可以了。这里也贴一个书里关于打印路径的算法。
PRINT PATH的参数为图G,起点s,终点v。π 代表的是父顶点,v.π 表示的是顶点v的父顶点,v.π = NIL 说明顶点v没有父顶点。这里的v.π 我们可以把它转换到数组里,其实表达的意思是一致的,但是为了方便表述,接下来都用书中的方法表示。同理,s到v的最短距离可以表示为v.d。
下面总结一下符号表示,v.d 表示s到v的最短距离,v.π 表示v的父顶点。v.π = NIL表示v没有父顶点。
现在直接进行一个Dijkstra的算法。
下面进入代码解读。
参数:图G,各个边的权重w,起点s。
第一行:初始化。对于所有的顶点v,我们先假设s到顶点的最短距离为∞,相当于最开始我们没有找到路径,∞就表示路径不存在。然后所有顶点的父顶点设成NIL,没有父顶点。最后s.d = 0,这也很好理解,从s到顶点s的最短距离为0,从自己到自己不需要距离。
第二行:初始化一个空集S。这个S是干嘛用的呢?这个S是用来储存那些已经确定最短路径的顶点的。注意一点,所有的表示都是基于顶点,当我们想知道顶点的最短路径,我们直接去访问那个顶点的属性(attribute),比如v.d,v.π。一开始没有任何确定最短路径的顶点,除了起始点,但我们先不急着把起始点放进去。
第三行:初始化一个集合Q,包括了图里所有的顶点G.V。这里的Q是一种特殊的数据结构,最小优先队列,键为各个顶点的最短距离,v.d,这样每次弹出的都会是最短距离最小的顶点。
第四行到第八行:
- Q中弹出从s到顶点最短距离最小的顶点u
- 把u加入S
- 对每个和u相邻的顶点,做一个“放松”的操作
我们发现我们一直在从Q中弹出顶点(简称Q弹),当顶点都弹完了,并且加入到S中时,整个算法结束,我们就能得到最开始所说的两个量,到此顶点的最短距离,以及此顶点的父顶点
“放松”是什么操作?本质上来说就是找到了一条更加好的路径,代替原来的路径。
RELAX:参数为顶点u, v以及各边权重。
如果v.d > u.d + w(u,v)? 这是什么意思呢?v.d 我们知道是当前到v的最短距离,如果这个距离大于某个距离,说明它其实并不是最短距离,为什么会这样呢,式子里u.d + w(u,v)是什么意思呢?u.d是当前到u的最短距离,w(u,v)是边 u->v 的距离,说明我们可以先到u,再到v,并且使这两段的距离之和比原来到v的距离短。如果式子成立的话,那么到v的最短距离v.d 就会变成u.d + w(u,v), 而v的父节点将变成u (因为路径中是从u 到 v)。
所以通俗上来说,Dijkstra的第7到8行表示的意思就是,我们想看一看有没有可能通过u,从u到v,来使得它相邻顶点v的最短距离变短,即v.d > u.d + w(u,v), 其实这里的相邻顶点应该取不在S中的顶点,相当于未探索的顶点,而不是所有的相邻顶点,因为有些相邻顶点的可能已经在S中了,那就说明该顶点的最短路径已经确定,不需要再进行操作了。更严谨一些应该是:
for each vertex v in G.adj [u] and v is not in S:
RELAX(u, v, w)
这样可以避免一些多余操作,当然你不加这个判定应该也没事,因为该顶点已经到达最优了,也不会进行“放松”操作。
综上所述,整个算法都是基于顶点的性质来进行的,比如v.d, v.π,还有图的表示是基于Adjacency list,这样的好处就是足够的抽象,足够灵活,让整个算法的表示非常的简洁明了。