単一ソースの最短パスについて考えています
前言:
最近一直在看《算法导论》。算法这块难啃的硬骨头,向来令我头疼不已,尤其是图算法这一部分愈发觉得难啃。在冥思苦想几日之后虽不能说豁然开朗,但也算是小有斩获,稍加整理思绪之后便有此文。所以以下的内容呢,源于我在阅读《算法导论》期间对自己思考过程的总结。
言归正传,今天我要说的主题是单源最短路径问题。比如说,你想知道怎样应该怎样从武汉去北京,那么你可以拿起你的手机登陆Google Maps,输入起点武汉,终点北京,Google会告诉你最佳的选择。撇开交通工具的差异(如果是飞机,估计您老就按照两点之间,线段最短的路径直接飞过去了),我们把地图上每一个城市想象成一个点,从一个城市去另一个城市的公路想象成一条线,那么怎样从武汉去北京的问题就变成怎样从起点经过这么多条不同的线路抵达终点的问题了。那么在如此错综复杂的地图上,我们应该如何寻求两点之间的最短路线呢?
很遗憾的是,当前还没有单纯的解决地图上从A点去B点最短路线的问题,我们往往需要求出从A点出发到所有地方的最短线路之后,才能确定从A到B的最短路线。这是一个很令人头疼的问题,你想如果我们从武汉去北京,犯得着思考怎样从武汉去广州吗?所以说目前的单源最短路径算法在这个境遇上显得十分笨拙。(其实这个问题被我严重化了,可以通过很简单的剪枝避免大量重复的计算)。
思考:
解决单源最短路径最通用的算法是Dijkstra算法,但是在此之前,我们还是先从Bellman-Ford算法说起。毕竟历史的轨迹是如此发展的,先有Bellman-Ford算法,后才有Dijkstra的改进算法。这样的顺序也方便于我们思考:解决同一个问题,我们是否能做得更好。
在正式进入出题之前,我们先给定单源最短路径问题的形式化定义:给定一个带权有向图G=(V, E),对于任意边(u, v)∈E,加权函数ω:E→R赋予边(u, v)一个实数权值ω(u,v)。计算出从给定源点s到图中其余顶点的最短路径。
另外对数据结构还有一些补充。对于任意顶点v∈V,除了存储顶点v至其邻接顶点的边之外,还存储了从源点s到顶点v的最短路径p=<s, v0, v1, …, vk, v>中v的前驱顶点vk和p的路径长度,分别用π[v]和d[v]表示,即π[v]=vk,d[v]=ω(p)。对于从源点到s到顶点v的最短路径,我们只用从顶点v递归寻找其前驱顶点,到s终止时即为其最短路径。我们用δ(s, v)表示从源点s到顶点v的最短路径长度。
对于这张图,我们设定源点为s,图中被阴影加粗的边都是最短路径所经过的边,
顶点内部的数值代表了其最短路径长度。
例如从s到z的最短路径p=<s, t, y, x, z>,其最短路径长度δ(s, z)=11
Bellman-Ford算法的基底来自于动态规划。根据《算法导论》中总结出是否能运用动态规划的两点特征:最优子结构和重叠子问题。我们可以证明最短路径问题是可以使用动态规划的,以下将用Cut & Paste这一技术来证明最短路径问题满足最优子结构。
最短路径的子路径也是最短路径: 假设p=<v1, v2, ...,vn>是从v1到vn的最短路径,对于任意i, j,其中1 ≤ i ≤ j ≤ n,那么在路径p中从顶点vi到顶点vj的子路径也是从vi到vj的最短路径。
我们把最短路径p分解为v1→vi→vj→vn,考虑从vi到vj的路径q,如果存在另外一条路径q'的路径长度比最短路径p中vi→vj的路径长度还要小,那么我们只用将这条路径q'替换掉原有最短路径p中vi→vj的部分,即可构造一个更短的路径。但这与我们假设p是从v1到vk的最短路径的前提矛盾,所以上述结论成立。至于重叠子问题就更好理解了,如果我们要求从v1到vk的最短路径,那么我们也需依次求出从v1到v2, v3, ..., vk-1的最短路径,每一次求解的过程中都会出现重叠的子问题。
在具体讲解Bellman-Ford算法之前,我们先引入三角不等式。因为正是通过不断的判定三角不等式是否成立,我们才能确切的求出从源点到图中每个顶点的最短路径。
三角不等式:对任意边(u, v)∈E,有δ(s, v) ≤ δ(s, u)+ ω(u, v)。
我们很容易就能证明三角不等式成立。对于从源点s到顶点v的最短路径p,假如存在另外一条经过顶点u到达v的路径p‘,并且p’比原有的最短路径p的路径长度更短,这就与最短路径的前提矛盾,所以不等式必然成立。我们在算法执行过程中始终维持三角不等式成立,并且不断修正顶点v∈V中d[v]和π[v]的值,继而求出最短路径,我们称这样的步骤为松弛(Relax),以下为松弛步骤的伪代码:
Relax(u, v, ω)
{
if d[v]> d[u] + ω(u, v)
then d[v] ← d[u] + ω(u, v)
π[v] ← u
}
Bellman-Fordアルゴリズム:
次は正式にBellman-Fordアルゴリズムです.ソース点sから図中の任意の頂点への最短パスが経験する辺数が|V|−1を超えないと判定し(ここで|V|は図中の頂点の個数である)、そうでなければループが現れる.最短経路で経験した辺数から出発し,ソース点sからボトムアップ構造で経験した辺数は1,2,...,|V|−1の最短経路は,終了時にソース点sから図中の任意の頂点への経路が最短経路であることが保証される.説明は難しくないようだが,次に数学的帰納法を用いてこのアルゴリズムの正しさを証明する.
ソース点sからの最短経路で経験する辺数nに対して、
基本手順:n=1の場合、ソース点sからその隣接頂点までのパスは、エッジ数が1の最短パスである.この点は、図中のソース点を除く任意の頂点vに対して、ソース点に隣接するか、その経路長がこのエッジの重み値であることが明らかである.ω(s, v);ソース点に隣接しないか、その経路長は∞(2つの頂点が互いに到達できないことを示す)である.
帰納手順:n=kのとき,ソース点sから出発し,経験辺数が1,2,...,kの最短経路.では、このような経路p=
以上より,辺数|V|−1を経由する最短経路を構築した後,ソース点sから図中の任意の頂点v∈Vへの最短経路を確立した.
あるいはより一般的には,sから図中の各頂点v∈Vにこのような最短経路が存在し,その経路長はδ(s,v)=min{p:pはsからvまでの到達可能な経路}であり,そうでなければ到達不可能であり,その経路長は∞である.経路pの順序に従って,sからvまでのサブ経路が毎回最短経路であることを保証すれば,最終的にはこのような最短経路を得ることができる.
しかし、私はMITの教育画面を見て、往々にして私たちはこのように|V|-1回実行してこそ、単一ソースの最短パスを決定する必要はありません.一方、図中のすべての頂点が確立した最短パスが経験した辺数は|V|-1に達しません.一方,アルゴリズム実行中にi回目まで実行すると,sからviまでの経路で経験したi辺を緩和するだけでなく,逆にすべての辺を緩和する.この結果,i回目の運転時に偶然の発見があったかもしれないが,sからviへの最短経路だけでなく,viからvjへの最短経路も確立した.では,sからvjへの最短経路を直接確立し,i回目以降のプロセスは実質的な影響を及ぼさないかもしれない(実際には余分で余分な時間オーバーヘッドである).
考えを変えれば、sから任意の頂点vへの最短経路が経験する各頂点の順序、すなわち最短経路p=
Dijkstraアルゴリズム:
今は私たちのメインイベントが登場する時だと思います:Dijkstraアルゴリズム.現在最も広く使用されている単一ソース最短パスアルゴリズムとしてDijkstraアルゴリズムは実行速度が効率的であるだけでなく,プログラム構造が優れていると言える.米中で不足しているのは,Dijkstraアルゴリズムが解く際に貪欲な戦略を採用し,貪欲な選択の性質を維持するために,アルゴリズムは与えられた重み付き有向図中のすべての辺の重み値が0以上であることを要求している.重み値が負のエッジが図に存在する場合、Dijkstraアルゴリズムはその正確性を保証できません.しかし、現実生活に目を向けると、このような問題を解決する場合、負の数はほとんど存在しないことがわかります.例えば、道路、街の距離は負の数ではありません.より厳しいことに,理論的にはこれは確実に存在するが,負の重みに割り当てられたエッジの実際のモデルを記述することは困難である.そのため、Dijkstraアルゴリズムは、単一ソースの最短パスの問題を実際の応用で解決する際に余裕があると言える.
Dijkstraアルゴリズムは、動的集合Sを維持することによって、最短経路が決定された頂点をSに絶えず追加し、Sが図中のすべての頂点を含んでいる場合、アルゴリズムは終了する.前述したように,Dijkstraアルゴリズムは,集合S外の頂点を選択するたびに,現在の経路長が最も小さい頂点を選択してSを加えるため,貪欲な戦略を用いた.現在のパスの長さが最も小さい頂点を絶えず選択することによって,アルゴリズムの終了時にソースポイントから各頂点へのパスが最も短いパスであることを満たすことが期待される.我々は依然として数学的帰納法を用いてこの事実の成立を証明している.
集合Sにおける頂点の個数nについて、
基本手順:n=1の場合、初期化時にソースポイントのパス長d[s]を0にし、残りの頂点のパス長を∞にするので、Sの唯一の頂点は必ずソースポイントsである.負のエッジは存在しないので、δ(s,s)=0,ソース点sは確かに最短経路を確立している.
帰納手順:n=kのとき,S中のk個の頂点の最短経路を確立したと仮定する.では、Sにまだ存在しない残りの頂点(すなわち、集合V−S)を順次巡回し、経路長が最も小さい頂点vを選択する.
2.1頂点vの経路長d[v]=∞の場合、集合Sのすべての頂点が頂点vに到達できないことを示す.また,頂点vは集合V−Sの中で経路長が最も小さい頂点であるため,集合V−Sの中のすべての頂点の経路長は∞であり,ソース点sから集合V−Sの中のすべての頂点に到達できないと断言できる.v'∈V-Sの場合、δ(s,v′)=∞であるため,頂点vの最短経路を確立した.では、n=k+1の場合も結論は成立する.
⒉頂点vの経路長d[v]≠∞の場合、集合Sには頂点vに到達する経路が存在することを示し、この経路p=
以上より,集合Sが図中のすべての頂点を含む場合,ソース点sから任意の頂点v∈Vへの最短経路が確立されている.
あるいは、より一般的には、現在のパスの長さが最も小さい頂点を選択するたびに、グローバル最適化が保証されるのはなぜですか.結局は前提の設定です.図には負の重みエッジは存在しません.集合V−Sの頂点の場合、経路長が最も小さい頂点vを選択することは、現在の経路長よりも小さい経路が頂点vに到達することが不可能であることを意味し、そうでなければ、その経路の重み値は負である.Dijkstraアルゴリズムの改良の多くは,最小経路長の頂点を選択することから始まり,二叉最小スタックで実現される優先キューの時間複雑度はO((V+E)*lgV)であり,フィボナッチスタックで実現される優先キューの時間複雑度はO(VlgV+E)に最適化できる.それ以外にも,従来のDijkstraアルゴリズムはプログラム構造上,さらに最適化できない.
以上の解析の過程で負の重み回路を避けてきたのは,与えられた図に負の重み回路が存在すれば,この回路を絶えず循環し,所望の任意の小さな経路長を得ることができるからである.このように最短経路は存在せず,その経路長は−∞でしか表現できないため,単一ソース最短経路問題も意味がない.
後記:
実際、以上の内容の大半は「アルゴリズム導論」の原書と変わらず、偏差や漏れがある.私の考えは、自分のこのような思考過程を記録して、考えを整理したいと思っています.結局、複雑なことをはっきり説明できると、多くの場合、あなた自身が本当に理解しています.もともと私もコードを貼って、具体的に実現したいと思っていました.しかし、書いているうちに、そうすれば、自分の考えではなく説明に重点を置くことに気づいた(うまく話せないのは別だが、どこにも行かないだろう).私の目的は単純で、単源最短経路の解法を徹底的に身につけて、これからのプログラマーの道のために基礎を築くことを望んでいます.