读《图算法,Robert Sedgewick》笔记 —— 最短路径
发表于:2008年4月1日 14时17分0秒阅读(1)评论(0)
本文链接:http://user.qzone.qq.com/40348105/blog/1207030620
【转】读《图算法,Robert Sedgewick》笔记 —— 最短路径
最短路径(Shortest Path)是在实际应用中非常有用的工具,将该问题细分,可以分为点到点最短路径(source-sink),单源点的最短路径(single-source),所有点到所有点(all-pairs)以及带负边情况下的最短路径。
为了简化我们的问题,设置以下几个限制: ? 有向图,无向图可以看作每条边对应两条方向相反的有向边 ? 无负环,可以想象,如果存在负环则路径值可不断减小,含负环的最短路径是NP的,暂不讨论。 单源点最短路径的简单算法 单源点最短路径(无负边)可以用Dijkstra算法较好的解决,其思想类似与求最小生成树(MST)的Prim算法。只是Dijkstra将优先队列的权由两点的边改为了从源点到下一点的路径: Prim : Priority= edge.weight() // 从v点到w点的weight Dijkstra: Priority= wt[v] + edge.weight() // 从源点到w点的值,wt[v]表示源点到v点的值 注意,wt保存的就是Priority。 类似于Prim,Dijkstra算法的复杂度主要取决于优先队列的实现,普通的Dijkstra算法能在线性时间内解决单源点无负边的最短路径问题,即复杂度为V2。采用了优先队列的数据结构后,可以在ElgdV - Elg2V之间解决问题(d表示采用d-堆)。 点到点的无负边最短路径也可以用Dijkstra来解决,从源点出发,当搜索到目标点时停止搜索。十分容易理解,我就不罗嗦了。 All-Pairs 的最短路径问题 正如大多数教材中所讲到的,求单源点无负边最短路径用Dijkstra,而求所有点最短路径用Floyd。确实,我们将用到Floyd算法,但是,并不是说所有情况下Floyd都是最佳选择。 对于没有学过Floyd的人来说,在掌握了Dijkstra之后遇到All-Pairs最短路径问题的第一反应可能会是:计算所有点的单源点最短路径,不就可以得到所有点的最短路径了吗。简单得描述一下算法就是执行V次Dijkstra算法,自然其复杂度在最优情况下可以是VElgdV。 Floyd可以说是Warshall算法的扩展了,三个for循环便可以解决一个复杂的问题,应该说是十分经典的。从它的三层循环可以看出,它的复杂度是V3,除了在第二层for中加点判断可以略微提高效率,几乎没有其他办法再减少它的复杂度。 比较两种算法,不难得出以下的结论:对于稀疏的图,采用V次Dijkstra比较出色,对于茂密的图,可以使用Floyd算法。另外,Floyd可以处理带负边的图。 无环网络的最短路径问题 无环网络(与DAG略微有区别)较带环网络而言,因为无环,所以边的正负是无所谓的。 记 得拓扑网络中有的点可能只做源点没有入度,那么在求最短路径中就搜索不到该点了,我们提出多源点的最短路径问题(Multisource shortest paths)了描述这种情况下求最短路径。其实我们可以将该问题转化为单源点的最短路径问题,只需加一个哑结点,其指向所有的无入度结点,来作为新的源, 而哑结点的所有边权为0。 在一般图(带正环)中求最长路径问题,类似在带负环图中求最短路径问题,是NP难的。不过在无环网络中却可以求最长路径,因为无环网络无正环,所以可以确定最长的路径,而且最长路径对于DAG很有意义。所以我们先考虑在DAG中求最长路径(最短路径算法类似): 利用DAG的强大武器——拓扑排序,可以避免Dijkstra中使用优先队列的损耗,因此效率高于Dijkstra。 只要按拓扑排序后的序列遍历每个结点,刷新每条边,最终就可以得到最长路径了。 求多源点的最长或最短路径问题可以在线性时间内解决,即复杂度E。 对于所有点的最短路径,我们当然可以运行V次上一段的算法来求,这样复杂度是VE。除此以外,还存在着无环图中求所有点的最短路径,而且它可以避免多次使用拓扑排序。 类似于求DAG的传递闭包问题,采用DFS和DP方法,可以得到一个有效的算法。伪代码如下: 储存边的二维数组 p; 储存double的二维数组 d; 递归的函数(Graph 图,int 遍历结点v){ 遍历从v结点出发的所有边: 边e=该边; int t=边的另一端; double w=边的权; 若d[s][t]>w ,更新d[s][t]=w,p[s][t]=e; 若t未遍历,递归遍历t; 遍历从t结点出发的所有边: 若d[s][i]>w+d[t][i]: 更新d[s][i]=w+d[t][i],p[s][i]=e; } 该算法复杂度为VE(适用于负边)。 欧几里德网络(Euclidean Networks)的最短路径问题 Euclidean Networks是指平面上有许多点,点与点之间的权值是这两点间的几何距离,这样的网络。 一般,Euclidean Networks都比较大,即点比较多,在这里只考虑求点到点最短路径,由于距离无负值所以不必考虑负边的情况。那么参考前面介绍的方法,用Dijkstra是最好的选择了,用PFS(优先队列)保存待检查的结点然后选择最短的处理。 由于Euclidean Networks的边是依据几何距离来决定的,所以我们采用几种特殊手段来加速算法,减小搜索范围。首先考虑PFS的优先权,Dijkstra中使用 w[v]+edge.weight()作为优先权,在Euclidean Networks中可以将待检查点到目标点的距离加入考虑范围,这是一种启发式搜索的思想。这样可以对Dijkstra做以下修改(s源点、d终点、 dist几何距离函数、v正在检查点、w待检查点): 1. 对wt[s]初始化为dist(s,d) 2. 优先权改为 ( wt[v] + edge.weight() + dist (w,d) - dist (v,d) ),即从s到w的路径加w到d的距离。 (熟悉启发式搜索的话,可以认为f'()= wt[v]-dist(v,d) , g'()=edge.weight()+dist(w,d), h'()=f'()+g'()) 这样修改的优点可以通过一个简单的例子来理解:假设目前正检查的结点v,待检查的有w1(离v很近,不在v到d连线上)和w2(在v到d的连线上)。如果 采用Dijkstra,按优先权先遍历w1,然后w2,最后d,求得的是v-w2-d。但如果修改优先级,则可以避免遍历w1,直接得到v-w2-d的结 果。如果在大规模的数据中采用该方法可以提快搜索找到目标解。 但是,启发式搜索只是加快达到目标解的速度,而不会节省任何时间和空间。 欧几里德启发式搜索作为A*算法的一种算法,当然需要一个界限函数来消除已不可能达到更优解的情况,也就是剪支。简单的方法是在找到一个可能解时,将优先权会大于可能解的待检查点全部忽略。 欧几里德启发式搜索的限界可以理解是一个以s为原点的大圆中,有一个以s和d为焦点,形状受目前的解所影响的椭圆。Dijkstra会对整个圆进行搜索, 而启发式搜索只对该椭圆(因为焦点在圆内,而且解在不停更新变小,所以可以想象椭圆相对于大圆十分小)进行搜索。所以无论是空间还是时间,启发式搜索都大 大优于一般Dijkstra。 实现欧几里德启发式函数的另一个方法是对边重新赋权。简单描述为:对每条边v-w更新,即加上dist(w,d) - dist(v,d) ,对于wt[s]的初始化仍然是dist(s,d)。然后运行标准的最短路径算法。对边重新赋权稍后还会用到。 其他问题 首先定义一种有用的技术手段——Reduction(可能翻译为衰减吧) Definition We say that a problem Areduces toanother problem B if we can use an algorithm that solves B to develop an algorithm that solves A, in a total amount of time that is, in the worst case, no more than a constant times the worst-case running time of the algorithm that solves B. We say that two problems are equivalentif they reduce to each other. 定义 如果可以从解决B问题的算法设计一种解决A问题的算法,且最坏情况下的时间总量不 超过B算法在 最坏情况下的运行时间的整数倍,则我们称A问题衰减为B问题。当两个问题可以 互相衰减时,称其等价。 比如Floyd和Warshall是如此相似,我们可以说传递闭包问题reduces to所有点最短路径问题(反之不可)。因此,传递闭包可以用Floyd求,而且复杂度与Warshall是相同的。 再如对于边无限制的网络中,求最长和最短边问题是等价的。 Job Scheduling(我就不对相关问题名称做翻译了)的问题,对于某个作业需要先完成其他几个作业,每个作业有时间属性,要求在某些限制条件下完成所有作业的最短时间。首先考虑没有任何限制条件的简单调度。 Difference constraints问题,对x0到xn一系列变量设置一些限制,每个限制指其中两个变量的差不小于给定常数。例如 x1-x0>=0.41 x7-x1=0.41 等等。 Linear programming问题,Difference constraints问题的一般化。 可以将Job-scheduling 问题reduces to Difference-constraints问题,当a工作必须在完成b工作后开始,那么可以有a-b>=Tb 的关系。 Difference-constraints中常数为正数时的问题equivalent to 无环网络的单源点最长路径。所以对于一般无限制的Job-scheduling问题可以用无环网络的单源点最长路径解决。 考虑限制了最后期限的Job-scheduling问题,前面已经说过Job-scheduling可以reduce到Difference- constraints,其实如果作业是带有期限的话就相当于Difference-constraints中有xi - xj<=dj,即 xj-xi>=-dj。也就是Difference-constraints中的常数可以为负。 对于作业有期限的Job-scheduling问题可以reduces to 带负边无负环的最短路径问题。 Reduce方法的另一个用途是判断某类问题是否是NP-Hard的问题,NP难的问题reduce to 另一个问题,则另一个问题是NP难的。 带负边的网络求最短路径是NP难的。 摘自《图算法》的一些Reduction A B A=>B implication example 1 easy easy new B lower bound sorting => EMST 2 easy tractable none TC=>APSP(+) 3 easy intractable none SSSP(DAG)=>SSSP(±) 4 easy unknown none 5 tractable easy A easy 6 tractable tractable new A solution DC(+)=>SSSP(DAG) 7 tractable intractable none 8 tractable unknown none 9 intractable easy profound 10 intractable tractable profound 11 intractable intractable same as 1 or 6 SSLP()=>SSSP(±) 12 intractable unknown B intractable HP=>SSSP(±) 13 unknown easy A easy JS=>SSSP(DAG) 14 unknown tractable A tractable 15 unknown intractable A solvable 16 unknown unknown same as 1 or 6 JSWD=>SSSP(±) Key: EMST Euclidean minimum spanning tree TC transitive closure APSP all-pairs shortest paths SSSP single-source shortest paths SSLP single-source longest paths (+) (in networks with nonnegative weights) (±) (in networks with weights that could be negative) (DAG) (in acyclic networks) DC difference constraints HP Hamilton paths JS(WD) job scheduling (with deadlines) 存在负边的最短路径 存在负边并不可怕,可怕的是有负环,所以我们给现在所要求的最短路径加个限制条件——无负环。提出以下三个问题: ? 无负环的网络中求最短路径 ? 负环检测 ? 套利交易问题(PKU上有两道习题 1238 2240 ) 回顾Dijkstra方法,它对于负边的情况无法处理,因为作为贪心思想的Dijkstra在面对负边时失去了最优子结构的性质,导致算法失败。 而Floyd依然可以应对负边的网络,并且Floyd可以在V3的复杂度下检测负环。 替代Dijkstra的方法是Bellman-Ford算法,可以在复杂度VE下检测负环和求出单源点最短路径。具体算法随便找本算法就能找到。 对边重新赋权(reweighting)不影响最短路径! 多么振奋人心啊,如果对边进行重新赋值不影响最短路径,那么我们可以消除所有的负边,这样不就又回到了无负边的网络了吗。 首先会想到的重新赋边权的算法是给每条边加一个常数值,但事实证明此法不通。简单的反例是a到b有两条路径,一条经过一个点长A,一条经过两个长B (<A)。再给边加过常量C后,前面一条的路径总长增加了C,后面增加了2C,若C>A-B,那么最短路径将从后面那条变为前面那条。明显影 响了最短路径。 正确的重新赋权法是在计算了一次任意点的Bellman-Ford后,对每条边加一个该边两端点的差值的方法。其原理是利用对于任意的那一点的相对性来保持权的一些特性。具体的我就不罗嗦了,仔细推导一下就可以知道了。 重新赋权法如下,其中wt是经过一次Bellman-Ford后的单源点最短路径,edge.w是边的另一端: 对于每一个点v: 对于从点v出发的所有边edge: edge.weight = edge.weight + wt[v] - wt[edge.w]; 由于改变了边的权值,关于要用到比较边大小的操作将全部失灵!补救方法是在计算完最短路径后,将权改回来。另外,该法仍然无法解决负环的问题。 既然有了重新赋权值的方法,解决所有点最短路径又有了新的算法(Johnson's algorithm): ? 对网络的源点(或任意点)运行Bellman-Ford算法 ? 检测到负环则停止 ? 对网络重新赋权值 ? 用Dijkstra算法求所有点最短路径。 Johnson 算法的复杂度是VElogdV,d=E/V,若E<2V,则d=2。 最后,对于无负边网络情况下,检测环比单源点最短路径简单、单源点最短路径比所有点最短路径简单。而在带负边的情况下,三个问题的最坏情况下的复杂度是一样的。 链接: PKU 1238: http://acm.pku.edu.cn/JudgeOnline/problem?id=1238 PKU 2240: http://acm.pku.edu.cn/JudgeOnline/problem?id=2240 Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=715625 |