最短路径之Dijkstra算法(摘自算法基础)【一些简单的解释,没有代码】

Dijkstra算法——从一个源点到所有其他顶点的最短路径

Dijkstra算法的两个特色:
1.所有边的权重一定是非负的。
2.图中可能包含环。

这里,我们假设源点为s,边(u,v)的权重是weight(u,v),我们想要计算出,对于每个顶点v,从源点s 出发到顶点v的最短路径上的权重和sp(s,v),以及从源点s出发的最短路径上在顶点v之前的那个顶点。我们将这两个结果分别存储在shortest[v]和pred[v]中。

程序 DIJKSTRA(G,s)
输入 G:一个有向图(包含具有n个顶点的集合V,m条具有非负权重的有向边的集合E)
s:集合V中的一个源点
结果 对于集合V中的每个非源顶点v,shortest[v]表示从s到v的一条最短路径的权重和sp(s,v),pred[v]表示在这条最短路径上出现在顶点v之前的顶点。对于源点s,shortest[s]=0,pred[s]=NULL。如果从s到v没有路径,那么shortest[v]为∞,pred[v]=NULL。
步骤
1.对于除了顶点s之外的任一顶点v,shortest[v]均被赋值为∞,将shortest[s]赋值为0,对于每个顶点v,将pred[v]均赋值为NULL。
2.令Q包含所有顶点。
3.只要Q不为空,执行如下操作:
A.在集合Q中找出具有最小shortest值的顶点u,并将顶点U从集合Q中移除。
B.对于每个与顶点u相邻接的顶点v:
i.调用RELAX(u,v)

我们开始分析DIJKSTRA程序的运行时间,在此分析需要花费多长时间将n个顶点加入到集合Q中,需要花费多长时间已确定哪个顶点具有最小shortest值,并将该顶点从集合Q中移除,以及需要多少次调整操作(当由于调用RELAX程序而使得shortest值和pred值发生变化)。
现在将这些操作命名如下:
• INSERT(Q,v):将顶点v插入到集合Q中。(Dijkstra算法调用INSERT程序n次)
• EXTRACT-MIN(Q):将具有最小shortest值的顶点从Q中移除,并且将该顶点返回给它的调用者。(Dijkstra算法调用EXTRACT-MIN程序n次)
• DECREASE-KEY(Q,v):执行调整操作并记录shortest[v]的值,对集合Q中的顶点v调用RELAX操作而使shortest[v]的值降低。(Dijkstra算法调用DECTREASE-KEY程序多达m次)
这3个操作在一起定义了一种被称为 优先队列的结构。
下面显示了DIJKSTRA程序的重写版本,其中明确地调用了优先队列操作。
程序 DIJKLSTRA(G,s)
输入,结果 同之前的DIJKLSTRA的输入,结果。
步骤
1.对于除了顶点s之外的任一顶点v,shortest[v]均被赋值为∞,将shortest[s]赋值为0,对于每个顶点v,将pred[v]均赋值为NULL。
2.令Q为一个空的优先队列。
3.对于每个顶点v:
A.调用INSERT(Q,v)
4.只要Q不为空,执行如下操作:
A.调用EXTRACT-MIN(Q),将u赋值为调用返回的顶点。
B.对于每个与顶点u相邻接的顶点v:
i.调用RELAX(u,v)
ii.如果调用RELAX(u,v)降低了shortest[v] 的值,那么调用DECREASE- KEY(Q,v)。

• 简单的数组实现
实现优先队列的最简单方法是将顶点存储在具有n个元素的数组中,如果当前的优先队列包含k个顶点,那么就将这k个顶点存储在数组的前k个位置上,并不需要以特定的顺序。除了需要数组之外我们还需要维持指定当前数组中顶点个数的一个量。INSERT操作很简单:将一个顶点添加到数组的下一个未占用的位置上,并且将表示数组中元素个数的那个量增加一。DECREASE-KEY操作更加简单。这两个操作均只需要花费常量时间。然而EXTRACT-MIN操作需要花费ø(n)时间,因为我们必须对当前数组中的所有顶点检查一遍已找到具有最小shortest值的顶点。一旦找出了这个顶点,对它进行删除操作就很简单:只要将位于最后面的顶点移动到要删除的顶点位置,并且将表示数组中元素个数的那个量减一即可。n次EXTRACT-MIN调用需要花费O(n^2)时间。尽管对RELAX调用会花费O(m)时间,但是m<=n^2,所以通过数组实现对优先队列的调用,Dijkstra算法会花费O(n^2)时间,而主要影响花费时间的操作是EXTRACT-MIN操作。


• 二叉堆实现
<二叉堆:除了是一个二叉树之外,它还具有三个额外的特征。1.树可能除了最底层外,其他层均是满的,而且最底层是按照从左到右的顺序依次填充结点的。2.每个结点包含一个关键字,表示在图的每个结点内部。3.关键字遵循堆的性质:每个结点的关键字小于或等于它的孩子的关键字。>
我们也能将一个二叉堆存储在一个数组中,根据堆的性质,带有最小关键字的结点必定位于位置1处,位于位置i处的结点的孩子位于位置2i和2i+1处,位于位置i的结点的上侧的结点——它的双亲——位于位置⨽i/2⨼处。当我们将一个二叉堆存储在一个数组中时,直接将二叉堆中的结点自顶向下一次存储到数组中即可。
二叉堆还具有另外一个重要的特性:如果它包含n个结点,那么它的高度——从根到最底层的叶子结点的边的个数——仅仅为⨽lgn⨼。因此,当我们沿着一条路径从根结点出发一直走到叶子结点,或者从叶子结点处走一直到根结点,仅仅需花费O(lgn)时间。
因为二叉堆的高度为⨽lgn⨼,我们均能在O(lgn)时间内完成这三个优先队列操作。
对于INSERT操作,在第一个可用的位置上添加一个叶子结点,随后,只要该结点的关键字小于它的双亲结点的关键字,就将该结点和它的双亲结点进行交换,并且将该结点向着根结点的方向向上移动一层。由于从叶子结点到根的路径上至多存在⨽lgn⨼条边,因此至多会执行⨽lgn⨼次交换,因此INSERT操作会花费O(lgn)时间。
对于DECREASE-KEY操作,我们使用同样的观点:减小相应顶点内关键字的值,随后将该结点向着根结点的方向向上冒泡直到能够保证满足堆的性质为止,这一操作也会花费O(lgn)时间。
对于EXTRACT-MIN操作,首先将根结点的内容保存起来并将它返回给调用者,随后将最后一个叶子结点(具有最大编号的结点)存放到根结点处,随后从根结点开始执行“向下冒泡”,如果孩子结点的关键字比该结点的内容小,就执行交换操作,直到能够满足堆的性质为止。最终,返回原始的已存的根结点的内容。再次,因为自根到叶子结点的路径之多有
⨽lgn⨼条边,因此最多进行⨽lgn⨼次交换操作,因此EXTRACT-MIN也会花费O(lgn)时间。
当Dijkstra算法使用二叉堆来实现优先队列操作时,插入操作会花费O(nlgn)时间,EXTRACT-MIN操作会花费O(nlgn)时间,DECREASE-KEY操作会花费O(mlgn)时间。(实际上,插入n个结点仅仅会花费ø(n)时间,因为初始时只有源点s的shortest值等于0,而其他顶点的shortest值均等于∞。)当图是稀疏的——边的数目m远远小于n^2——使用二叉堆来实现优先队列比使用简单的数组更高效。另一方面,当图是稠密的——m接近n^2,此时图中包含很多边——Dijkstra算法中花费在DECREASE-KEY上的调用会花费O(mlgn)时间,这会使得使用二叉堆来实现优先队列执行速度比使用一个简单的数组实现优先队列的执行速度更慢。

我们还知道关于二叉堆的另一个特性:二叉堆能在O(nlgn)时间内完成排序操作:
程序 HEAPSORT(A,n)
输入 A:一个数组
n:待排序的数组A中的元素数量
输出 一个数组B(包含数组A中的元素,并且是排好序的)
步骤
1.根据数组A中的元素建立一个二叉堆Q
2.创建一个数组B
3.令i从1到n依次取值:
A.调用EXTRACT-MIN(Q),将B[i]赋值为调用的返回值
4.返回数组B
<第一步将输入数组转换为一个二叉堆,我们能使用两种方式来实现这个操作,一个方法是令二叉堆初始时为空,随后将数组中的元素一次添加到堆上,这会花费O(nlgn)时间。另一种方法是利用自底向上的方法把一个数组调整为堆,这仅仅会花费O(n)时间。通过对堆进行原址排序也是可能的,此时我们不再需要额外的数组B。

• 斐波那契堆实现
过于复杂,不做讨论

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值