算法导论-最短路径

 最短路径

这两天看算法导论看到了最小路径这部分内容,现在看起来越来越费劲,真心觉得图这种数据结构真的太难,主要是书上有太多的证明,看起来难受,都不知道能不能坚持看下去,闲话少说,今天聊聊最短路径方面的知识。要算最小路径,必不可少要掌握图里面的一下知识。

关于图有好多知识,我主要讲讲图的两种储存方式。

  • 邻接矩阵
  • 邻接链表
邻接矩阵就是以矩阵来表示图结构中节点与节点之间的连接。对于无权重图,通过0,1的方式表示节点之间是否连接,对于权重图,邻接矩阵对应元素的值就是节点与节点之间的权重。如下图所示:

                        

邻接链表是有V条链表组成,链表中的每个节点都与头节点相连接。通过指针的方式表示连接线。如下图所示:

                             

这两种储存方式各有优势,就空间复杂度而言,邻接矩阵为,邻接链表为邻接矩阵表示起来方便,适用于稠密的图;邻接链表适用于稀疏图。

最小生成树

最小生成树问题其实在我们生活中经常遇到,比如你出去外面旅游,在众多的景点中你肯定希望走最短的路将所有的景点逛完,注意这里是将所有的景点逛完的最短距离,而不是两个景点之间的最远距离(当然两个景点之间的最远距离后面也会说),换句话说就是在图中找到连接所有顶点,所有边的最小权重。既然知道了概念,我们就给最小生成树下个定义。
输入:连通无向图G=(V,E)和一个加权函数W,我们假设权值互异(边与边之间权值不相等)。
输出:一棵连接所有顶点的生成树,且权值最小。
例:
图中加粗的边组成的树就是最小生成树。

最优子结构

(友情提醒,这部分是从原理上解释最小生成树算法的,不想懂原理的不要看,就认为我这是装逼)

定理1:T是图G中的最小生成树,G_1是一个由T_1顶点导出的图G的子图,V_1为T_1的顶点,E_1是顶点对的集,而x,y有E1={(x,y)属于E;x,y属于V1},则T1是子图G1的最小生成树。

这个定理说起来非常绕口,其实很简单,也好理解。就是有一个图,找到了该图的最小生成树,从最小生成树里面截取一棵子树,该子树是子图的最小生成树。该定理我就不证明了,用剪切法很简单。

图中阴影部分的T1和T2都是图G子图的最小生成树。

有了该定理我们就可以得到一个递推公式:

看到这个递推公式首先想到的就是动态规划了,但是这时候我们先不要着急就用动态规划去做,因为满足动态规划也可能满足贪心算法,而贪心算法比动态规划的效率更高,而且简单。而最小生成树问题正好可以用贪心算法来解决,为什么??下面这个定理给你解释解释。

定理2:设T为图的最小生成树,令A为V的任意子集,,假设(u,v)是连接A与V-A(A的补集)的最小权值边,则有边

同样该定理通过剪切法也能证明,我就不赘述了。但是定理2确实保证了贪心算法解决该问题的正确性,贪心算法的本质是局部最优从而达到全局最优,而这里边保证了全局最优。

算法

前面讲的所有的都是为这里做铺垫,干货来了。最小生成树算法有两种:1、Prim算法 2、Kruskal算法。两种算法的本质都是贪心算法。

Prim算法 

伪代码:
上面的伪代码可能不会感到有些迷糊,下面看看其具体的实现过程:

我们根据图中具体的例子来说明Prim算法,如图a所示,随机找到一个源节点a,将a加入到集合A中,找到集合A与集合A的补集连接的边中权重最小的那条边(a,b),并将节点b假如到集合A中,重复上述操作找到集合A与集合A的补集连接的边中权重最小的那条边(b,c),将节点c加入到集合A中,知道所有节点都在A中时停止循环.

在采用二叉堆的数据结构情况下,该算法的时间复杂度

Kruskal算法

伪代码:

Kruskal算法用到了不相交集合的数据结构(简称并查集),对这个数据结构不了解的同学可以看看我的这篇博客:http://blog.csdn.net/hxlove123456/article/details/78446225

Kruskal算法也是每次找最小边,但是找的方式有些不同。该算法的基本思想是,首先将每个节点看出是一棵树,这样就有了V棵树,每次从图中所有边中找到最小权重的那条边,通过这条边将包含该边两个节点的树进行合并,当然如果两个节点在一棵树中,就不需要合并,跳过该操作。当V棵树变成一棵树的时候,停止循环。换句话说,就是我们有很多的最优子结构Ti,通过最小权重边将最优子结构进行合并,最后合并成为一棵树,该树就是最小生成树。最后该算法的时间复杂度为


单源最短路径


对于单源最短路径,也有四种情况:

无加权情况:广度优先搜索时间复杂度:

无负权值情况: Dijkstra 算法时间复杂度: 

一般权值情况:Bellman-Ford算法时间复杂度:

DAG(无环):拓扑排序后运行一次Bellman-Ford算法时间复杂度:


广度优先搜索


伪代码:
广度优先搜索的实现过程:
广度优先搜索和prim最小生成树差不多,区别就是广度优先搜索采用了最小优先队列,
首先从源点s出发,修改连接源点s节点的权重,并将其节点w,r压入优先队列中,而
后w出队列,修改连接w节点的权重值,并将和w连接的节点压入队列,而后节点r出队
列,重复w的操作,一直循环知道队列中没有元素停止。

Dijkstra 算法

伪代码:


实现过程:
根据上图我们来进行分析:其实Dijkstra算法的实现过程和广度优先搜索过程类似,区别就在
于Dijkstra算法解决的是有权树,储存结构用的是优先队列,而广度优先搜索用的是先入先出
的队列。
首先将节点s赋值为0,其他节点赋值为无穷大,而后从源点s出发,对与s相连的节点y和t权
值进行松弛操作 )并更新,所以y权值变成5,t的权值变为10,由于
节点y的权值比t的权值低,故将y取代s作为源点,继续对与y相连的节点进行松弛操作,直
到所有节点都进行了一遍松弛操作之后,得到节点s的单源最短路径。
算法的运行时间: ,所以采用不同的数据结构会有不同的时间复杂度。


我们平常说 Dijkstra算法的时间复杂度为,是因为用斐波那契堆的效果最好。

Bellman-Ford算法

伪代码:

该算法的运行过程如下:


上面图中#i代表每条边更新的次序,换句话说就是节点权值的权值的更新是按照事先给定的顺序对每条边进行松弛操作,比如#1是节点B指向节点E,意思是循环开始首先通过节点B的权值来更新节点E的权值,一共要循环V次。注意,利用Bellman-Ford算法还可以检测是否存在负权环,检测的方法就是对每条边在循环一次Bellman-Ford算法,比较两次算法出来的结果是否一样,如果一样说明没有负权环,否则存在负权环。该算法的时间复杂度为
讲到Bellman-Ford算法就不得不提一下线性规划问题(LPS)了。

线性规划

给定一个矩阵A,m阶向量b,n阶向量c,找到一个n阶向量x,使得cx最大,但是要满足约束Ax<b??
首先我们对该问题做一个简化:对于矩阵A的每行,有一个1,有一个-1.其他为0,换句话说就是每行只存在两个元素,有式子
例:

约束图
可以 转化为
故上述不等式可以等价于:

在约束图的基础上增加一个源节点s,s出发到各个节点边的权重为0,如下图所示:

由此我们可以求原点s到各个节点的最短距离,该距离就是差分约束方程的可行解。

全对最短路径(找到所有点对点之间最短距离)

(1)无加权:运行V次广度优先搜索    时间复杂度为 
(2)非负权:运行V次Dijkstra     时间复杂度为
(3)一般情况: 运行V次Bellman-ford算法    时间复杂度为 
对于无加权和非负权值情况,时间复杂度还可以接受,但是一般情况时间复杂度太高,我们来对其进行改造,降低其时间复杂度。

动态规划

动态规划总结起来就是寻找最优子结构,换句话说就是找到从步骤k到步骤k+1的递归表达式。
下面给出一个定义,表示从节点i到节点j的最短路径最多用m条边。则有:
且有
利用上面的定义,我们可以得到如下递推式:
看到上面的递推式,是不是感到疑惑??凭什么说节点i到节点j的最短距离就要m条边,我也有可能是m-1条呀!!这样想是对的,加入我们有m-1条边到达节点j,那么我们可以得到,而ajj是节点j到节点j的距离等于0,所以上面的递归式也成立。换个角度来看上面那个递推表达式,其实就是通过一些已知边的最小路径往外扩展,是我们在已知i到k在最多m-1条边的基础上推导到i到j上的最短距离,这样说不知道能不能懂。
由此得到动态规划的伪代码:

由上可知,该算法的时间复杂度为。和上面的算法的时间复杂度差不多。但是该方法就像一个引理,引出Floyd-Warshall算法。

Floyd-Warshall算法

因为Floyd-Warshall算法是在动态规划上进行优化的,所以我们来分析一下动态规划的缺点,我们才好进行优化。由动态规划的原理可以知道,我们根据算法前面计算出来的前一个节点的最短路径从而推导目标节点的最短路径,这样做比较繁琐。我们能不能知道节点i和节点j,以及中间的某个节点k,而我们知道了节点i到节点k的最短距离和节点k到节点j之间的距离,我们就可以直接将两者相加不就是节点i到节点j的最短距离嘛,不需要非得求前一个节点的最短距离。有了这个思路,我们就开始讲Floyd-Warshall算法。
定义表示节点i到节点j最短路径的权重,k表示最短路径最多k条边。
则有递归式如下:

因为我们最短路径不一定是k条边,所以我们需要这里存在
伪代码如下:

从上可以看出,该算法的时间复杂度为,已经还可以了,但是没有最好,只有更好,下面介绍我们的终极算法。

Jahnson 算法

由于DIjkstra算法的时间复杂度为,而有不等式,所以DIjkstra算法的时间复杂度又可以表示为,但是DIjkstra算法只能针对无负权值的情况才能够有用,所以我们就像能不能对图中的权值进行一个变形,其实开始我想直接对每条边的权重加上所有边加上一个固定值,使得所有权值都变成正数,但是后来发现因为无法知道最短距离的边数目,所以无法转化回去,所以这个方法就当我意淫了,下面介绍正统方法。

权重调整

对于每条边的权重,有权重函数,表达式如下:

这里的h(u)和h(v)我们可以看成是节点也有一个权值,即节点u的权值是h(u),这里我们可以证明对于所有的u到v的路径,我们的表达式都是不变的。

证明

为一条路径,则有

这里告诉我们需要找到这样的一个节点的权重h(这里我们看成图中边有一个权重,节点也有权重,可能更好理解),使得所有节点之间的权重都是非负的,然后使用Dijkstra算法,计算出最短距离,最后调整回原来的值。那么我们怎么来找到这样的一个权值呢??可以知道:

看到推导出来的式子有没有想到我们线性规划里面的约束,把这个式子套用要线性规划里面的知识,利用Bellman-Ford算法就可以求取每个节点的权值函数h。
下面来总结一下Jahnson 的步骤:
1.找到一个函数h,将V映射到R,对每条边的权重进行调整,使得图中每条边都是非负的。
2.运行Bellman-Ford算法,解决差分约束问题。
3.在权重为Wh上运行Dijkstra算法V次,得到全对最短路径。
4.对每对对点,计算原来的最短距离
到这里为止,关于最短路径所有的知识就讲完了。讲的不正确的地方望指正。




  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值