图论系列之最短路径

图的最短路径是图论里非常常见的问题,无论是平时刷题,还是实际领域里,很多都会涉及到图论中的最短路径。比如,分布式系统,复杂网络等。特别地,在GIS(地理信息系统)里,最短路径的问题更是很常见了。

总的来讲,图论中的最短路径大致分为两种:第一种为单源最短路径,第二种为每对顶点之间的最短路径。

单源最短路径

对于单源最短路径,我们可以根据图是否带权以及权重值的正负来选择不同的算法。这里主要讲三种基本的算法:BFS算法,Bellman-Ford算法和Dijkstra算法。这三种算法各有各的使用条件,也各有各的用处。

BFS算法

BFS算法为图论里的一个基本算法,即广度优先搜索。它的搜索行为是这样的:当搜索到达当前节点s时,如果s有邻居节点,那么算法优先访问这些邻居节点,当所有这些邻居节点被访问之后,如果这些邻居节点还有各自的邻居节点,那么再按照上述方式继续访问。这种搜索方式看上去像是一层一层地去遍历图中的节点,这就为BFS算法求解无权图中的单源最短路径提供了可能性。

在一个无权图(有向图或是无向图皆可)中,图的每一条边都不带权重,可以看成1。对于在这种图里求解单源最短路径,BFS算法可以很好地解决。通过在图上使用BFS算法,就可以得到给定源点s到图中其它任意一点的最短距离,而该距离就是任意一点距离源点s的层数或最少边数。

因为只是谈概念讲理论的话,是没有办法深入理解一个算法的,毕竟理论是干瘪的,只有结合概念和实际的一些应用,才能很好地理解一个算法。所以有兴趣的读者可以看看poj2251和poj3278,这是链接地址:

http://poj.org/problem?id=2251

http://poj.org/problem?id=3278

 

如果一个图是带权的,那么BFS算法就不管用了,因为从源点s到任意一个点的最少边数不一定就是它们之间的最短路径了。因此,我们将要介绍下面两种算法:Bellman-Ford算法和Dijkstra算法。其中,对于含有负权的图,只有Bellman-Ford可以解决,而Dijkstra算法只适用于所有边的权不为负的情况。这里特别注意:两种算法都不适用于带有负权回路的图,但是Bellman-Ford算法可以检测出这个负权回路。

Bellman-Ford算法

Bellman-Ford算法能在图中存在负权边的情况下,解决单源最短路径问题。对于一个带权图,在其上运行该算法后可以返回一个布尔值,表明图中是否存在一个从源点可达的负权回路,若存在这样的回路,那么从源点到某些点的最短路径可以通过这个负权回路无限制地松弛下去,进而得不到从源点到该点的最短路径;若不存在这样的回路,那么算法将产生最短路径及其权值。

关于Bellman-Ford算法,其时间复杂度为O(VE),其中V代表图中顶点数,E代表图中边数。这个时间复杂度实际上是较高的。主要原因是算法要进行V-1次迭代,并且每次迭代都要对E条边进行松弛。为此,有人提出了一个Bellman-Ford算法的优化改进算法,即SPFA算法(Shortest Path Faster Algorithm)。该算法用一个队列作为优化,减少了冗余的松弛操作,从而改进了Bellman-Ford算法的时间复杂度,因此,在实际编程中,很多时候都用SPFA算法来代替Bellman-Ford算法。SPFA算法具体的实现步骤这里不做详细探讨。

关于Bellman-Ford算法的实际应用,除了求解带有负权但不含负权回路的单源最短路径之外,还有一个特别的应用。上面提到了Bellman-Ford算法可以检查图中是否含有源点可达的负权回路,基于这个理论有一个很实际的应用:套汇问题。该问题的描述是:利用货币汇率的差异,把一个单位的某种货币转换为大于一个单位的同种货币的方法。比如,假定1美元可以买46.4印度卢比,1印度卢比可以买2.5日元,1日元可以买0.0091美元。通过货币兑换,一个商人可以从1美元开始买入,得到46.4*2.5*0.0091=1.0556美元,因而获利5.56%。(注:上面这个例子摘自《算法导论》)。

现在给出一个问题:给定一系列货币以及它们之间的汇率,从某种货币开始,能否经过一系列的货币兑换得到的钱数大于这种货币。

实例分析:我们可以把问题中的每种货币看作是图中的一个节点,货币A到货币B的兑换看作是从A点到B点的一条有向边,各条有向边的权值即是汇率。这样一来,问题转化成在一个有向图中,判断是否存在从源货币可达的正权回路,如果有这样的回路,那么货币在这个正权回路中将持续增值,最终再兑换回源货币时,可以实现增值。而这正是Bellman-Ford算法的逆应用。把Bellman-Ford算法中的判断源点可达的负权回路稍作改动,就能解决这个问题。有兴趣的读者可以看看poj1860。亲自动手写写程序,相信对Bellman-Ford算法的理解会更深刻。这里是链接:

http://poj.org/problem?id=1860

 

Dijkstra算法

当带权图的权值皆为非负时,虽然也可以用Bellman-Ford算法,但是其时间复杂度太高,所以在这种情况下,我们可以使用Dijkstra算法。

Dijkstra算法采用的是贪心策略。算法初始时设置了一个顶点集合S,该集合最开始为空。然后初始化源点s到图中各点的最短路径估计值,记作d[v],其中v为除源点以外的任意点。初始时d[s]=0,其它点的d值均为正无穷大。d值的维护用到了最小优先队列。算法每次选择d值最小的点,然后将该点加入集合S,并对该点的所有出边进行松弛。

Dijkstra算法的时间复杂度主要依赖于最小优先队列的实现,如果最小优先队列的实现仅仅是一个简单的数组,那么每次找最小的d值都需要O(V)的时间,而一共有V个d值,这里V是顶点个数。所以算法总的时间复杂度为O(V2)。这也是Dijkstra算法最坏情况下的时间复杂度。但是一般情况下,如果最小优先队列设计得较好,Dijkstra算法的时间复杂度会低于Bellman-ford算法的时间复杂度。

关于Dijkstra算法的实际应用请参见poj2387和poj3268。两道题目都是非常典型的dijkstra算法的应用。这里是链接地址:

http://poj.org/problem?id=2387

http://poj.org/problem?id=3268

这里需要说明的是:由于Bellman-Ford算法的适用条件比Dijkstra算法的适用条件要宽松,因此Dijkstra算法适用的场景也适用于SPFA算法。由于SPFA算法的实现相对较简单,并且时间复杂度低,因此也可以用SPFA算法来代替Dijkstra算法。

 

每对顶点之间的最短路径

 

对于每对顶点之间的最短路径,我们直观的想法是:如果顶点个数为n,那么以每个顶点作为源点,调用上述的单源最短路径算法n次,就可以解决这个问题。的确,这是一种解决的思路,但是这种解决方式的时间复杂度会很高,一般不推荐这样做。

Floyd-Warshall算法

通常采取的思路便是Floyd-Warshall算法了,这个算法采用的是动态规划的思路,算法的时间复杂度为O(V3),其中V是图中顶点个数。它的实现方式也十分简单,只需写一个三重循环就能搞定,但要注意的是每重循环的含义,这里可以结合Floyd-Warshall算法的动态规划设计思路来加以理解。

需要注意的是Floyd-Warshall算法同样不适用于带负权的回路,但可以允许有负权。说到这里,我们自然会问,Floyd-Warshall算法如何来判断图中是否有负权回路?这里有一种方式如下:当执行完该算法后,会得到一个最终的结果邻接矩阵,我们只需判断结果邻接矩阵的主对角线的元素是否有为负值的,如果有,则该图存在负权回路,如果没有,则说明该图不存在负权回路。

由上述,我们可以自然地引出Floyd-Warshall算法的一个应用,套汇问题。那么读者可能会有所疑问,套汇问题不是Bellman-Ford算法的一个应用吗?其实这里并不矛盾,如果套汇问题给定了初始的币种,让判断最后能否获利,这样类似于求单源最短路径,因此选用Bellman-Ford算法;但如果问题没有给出初始币种,只给了币种之间的汇率,让判断在这些汇率之间进行套汇能否获利,这时的问题就类似于求每对顶点之间的最短路径问题,这个问题当然可以执行n(n为顶点个数)遍Bellman-Ford算法来解决,但我们之前已经讲了,这时Floyd-Warshall算法的效率更高。

关于Floyd-Warshall算法,建议大家可以去做一些题,体会一下该算法的实际应用场景,对于更加深刻地理解它有帮助。poj1125和poj2240(注意和poj1860进行对比)大家可以去做做。这里是链接地址:

http://poj.org/problem?id=1125

http://poj.org/problem?id=2240

 

对于本文所总结的算法,它们的实际应用远远不止文中提到的,很多实际问题,经过巧妙的转换,都可以归结为图论中的最短路径问题,在完全掌握了算法的原理之后,如何提升对于算法灵活运用的能力,大概就只有多编程这条路了,只有见得多了,做得多了思路才会被打开!

本文对图论中的最短路径算法进行了一个总结,主要注重的是算法的适用场景以及强调了算法的实际应用而对于算法的具体实现步骤以及正确性的证明却没有涉及。如有不足之处还望大家留言指正,谢谢!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值