读《图算法,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算法能在线性时间内解决单源点无负边的最短路径问题,即复杂度为V
2。采用了优先队列的数据结构后,可以在Elg
dV - Elg
2V之间解决问题(d表示采用d-堆)。
点到点的无负边最短路径也可以用Dijkstra来解决,从源点出发,当搜索到目标点时停止搜索。十分容易理解,我就不罗嗦了。
All-Pairs 的最短路径问题
正如大多数教材中所讲到的,求单源点无负边最短路径用Dijkstra,而求所有点最短路径用Floyd。确实,我们将用到Floyd算法,但是,并不是说所有情况下Floyd都是最佳选择。
对于没有学过Floyd的人来说,在掌握了Dijkstra之后遇到All-Pairs最短路径问题的第一反应可能会是:
计算所有点的单源点最短路径,不就可以得到所有点的最短路径了吗。简单得描述一下算法就是执行V次Dijkstra算法,自然其复杂度在最优情况下可以是VElgdV。
Floyd可以说是Warshall算法的扩展了,三个for循环便可以解决一个复杂的问题,应该说是十分经典的。从它的三层循环可以看出,它的复杂度是V
3,除了在第二层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待检查点):
- 对wt[s]初始化为dist(s,d)
- 优先权改为 ( 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 A
reduces to another 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
equivalent if 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