开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第19天,点击查看活动详情
7.1.3 有权图的单源最短路
由图得知从红色的位置到绿色的最短路径是哪条?
是这条啦:
有权图跟无权图最短路的区别:
有权图的最短路不一定是经过顶点数最少的那条路,上图就很好的证实了这点
上图的最短路上的全重合时1加8等于9.但有权图的最短路是1+4+1=6(权重更低)
如果路径上的权重还有负数的话,是不是最短路又会发生改变,比如这样:
这个图我如果不停的循环,每圈赚5块,那无限转圈不就反而倒赚正无穷(美好的愿望hh)
这种情况叫做有一个负值圈叫做(negative-cost cycle)
- 图里面要是有这样一种负值圈的话,基本上所有的算法都会挂掉,所有后面不考虑这种情况
- 算法相通之处:按照递增(非递减)的顺序找出到各个顶点的最短路
Dijkstra算法
跟BFS相似的地方在于他都是把顶点一个个往那个集合里面收
-
令S = {源点s + 已经确定了最短路径的顶点vi}
-
定义一个距离的数组叫做dist:对于任何一个没有被收入的顶点v,我们把dist v定义为 什么呢?定义为源点到这个v的最短路径长度,但是这不是最终的最短路径,但是该路径仅经过s中的顶点。即路径{s->(vi属于S)->v}的最小长度(多半不是我们真正想要的最小长度,但随着一个个顶点不断被加到这个集合里面,这个dist v会慢慢的变小变小,直到最后被完善成那个最短路径),当他成为最短路径后,这个v就被收到了这个集合里面
-
算法能够这样执行很重要的前提:路径是按照递增(非递减)的顺序生成的,则
-
真正的最短路必须只经过S中的顶点(为什么?)
-
采用反证法:
假如我们下一个要把v收进去的时候,从s到v的路径上还存在另外一个点w。这个w是在这个s以外的,那想要从s到v必然要先到达w再到v,这个时候就会有矛盾,s到w的距离显然小于s到v的距离,而v是下一个马上就要被收进去的顶点,我们的路径是按照递增顺序生成的,这就是意味着凡是距离比v要小的那些顶点都应该在他之前就已经被收进去了,w到s的距离显然比v到s的距离小,所有w肯定应该已经在s这个集合里面了,不可能在外面。所以就得出上述的结论 -
每次就从未收录的顶点中选一个dist最小的收录(贪心算法)
-
增加一个v进去S,可能影响另外一个w的dist值:得到的两个重要的事实在下面的图中选项B里
-
-
1不仅在w的路径上,而且从v到w必定存在一条直接的边(意思就是收w一定是v的邻接点。况且v被收录进去能影响的也就他一圈的邻接点了,所以收录进去的时候看看这个值周围一圈邻接点看谁有没有比他更小的) 为什么不可能是从s先到v,然后v再经过另外一个顶点再到w呢?这种情况是不可能的,因为路径是按照递增顺序生成的,如果v和w之间还有另外一个顶点的话,那么这个顶点到源点的距离一定比v到源点的距离要大,但是我们假设的是w的dist值应该是从s到w,那条这条路径的长度仅仅经过这个集合里面的顶点。如果另外还有一个节点在v后面的话,那这个顶点不可能在s里面,因为v是新增进去的,v应该是里面集合最长的- 更小的dist值可能为:dist[w] = min{dist[w],dist[v] + <v,w>的权重}
-
Dijkstra算法框架
void Dijkstra(Vertex s)
{
while(1){
V = 未收录顶点中dist最小者;
collected[V] = true;//收到集合里面
for( V的每个邻接点 W )
if( collected[W] === false ){
if( dist[V]+E<v,w> < dist[W] ){//this不能随便初始化,因为这个不等式在的原因,我们当描述一个顶点跟s没有直接的路可以通的时候,一定要把它定义成正无穷,这样当我们发现一个更短路径的时候,才可以把这个往更小的地方去更新(假如我们随便把它定义成-1的话,那这个不等式永远都不会成立了)
dist[W] = dist[V] + E<v,w>;//<v,w>是下标
path[W] = V;
}
}
}
}//Dijkstra算法不能解决有负边的情况,因为这个算法的思路是按照距离从小到大的顺序去收集每一个顶点,如果有一条边是负的,那么对于某一个w来说就有可能说就有了一个dist v 然后他减掉了一个正值,我们就可以得到一个比v还要短的w。然后w之前是排在v的后面的,所以整个算法会乱掉
假如有边的话?要怎么做
Dijkstra算法的时间复杂度:不科学,没有欸。因为在上面的代码中只是一个伪代码演示
在 dist[W] = dist[V] + E<v,w>; 中说的是如果我有一个更短的距离,我要把这个值更新为这个最短的距离。这个更新不一定是一个简单的赋值
方法1:直接扫描所有未收录顶点 -O(|V|)//这个的时间复杂度就是OV
//使用方法1这种粗暴的方式的话那后面的赋值语句真的就只是一行了。得出来的整体复杂度就是 T = O(|V|²+|E|),扫描一遍是V个,一共扫描V遍且扫描的时候涉及边的每个邻接点,也就是每条边又被访问了一遍
//对于稠密图效果好,稠密图是指那些有很多边的(边的条数跟顶点的个数是OV平方数量级的)
方法2:将dist存在最小堆中 -O(log|V|)
//每次只要把堆的根节点弹出来,然后调整这个最小堆,每次获得最小距离的这一步操作是一个logv的时间复杂做的算法,比前面OV快多了
//更新dist[W]的值 -O(log|V|)变成稍微复杂的事情,不仅要把值更新,还得把它插回那个最小堆里面去
//总体复杂度:T = O(|V|log|V| + |E|log|V|),这个堆稀疏图效果好,如果是稠密图的话可以将复杂度写成elogv
稀疏图:e跟V是同一个数量级的,不是V平方数量级的,复杂度就是vlogv
7.1.3-s 有权图的单源最短路示例
void Dijkstra( Vertex s )
{
while(1){
V = 未收录顶点中dist最小者;
if( 这样的v不存在 )
break;
collected[V] = true;
for( V的每个邻接点W )
if( collected[W] == false )
if( dist[V] + E<v,w> < dist[W] ){//小于正无穷就执行,dist[W]还未经过的点是默认为正无穷
dist[W] = dist[V] + E<v,w>;
path[W] = V;
}
}
}
下图中的dist表示源点到目标点之间的权重,path表示当前点的上一个点的下标
刚开始:正式进入Dijkstra算法:
进行邻接点排除前:
排除完后: