ダイクストラ法の動作のアニメーション
ダイクストラ法 (だいくすとらほう、英 : Dijkstra's algorithm )はグラフ理論 における辺の重みが非負数の場合の単一始点最短経路問題 を解くための最良優先探索 によるアルゴリズム である。
ダイクストラ法は、1959年エドガー・ダイクストラ によって考案された。
応用範囲は広くOSPF などのインターネット ルーティングプロトコル や、カーナビ の経路探索や鉄道の経路案内においても利用されている。
ほかのアルゴリズムとして、
最短経路長の推定値を事前に知っているときは、ダイクストラ法の改良版であるA*アルゴリズム を用いて、より効率的に最短経路を求めることができる。
辺の重みが全て同一の非負数の場合は幅優先探索 がより速く、線形時間で最短路を計算可能である。
無向グラフで辺の重みが正整数の場合は、Thorupのアルゴリズム[ 1] によって線形時間での計算が可能であるが、実用性はあまり高くない[要出典 ] 。
辺の重みに負数を含む場合はベルマン-フォード法 がある。
ダイクストラ法の擬似コードは以下の通りである。始点(スタート)から全てのグラフの頂点への最短経路を求める。
Q
{\displaystyle Q}
は頂点の集合。
u
{\displaystyle u}
,
v
{\displaystyle v}
は頂点。
グラフ
G
=
(
V
,
E
)
{\displaystyle G=(V,E)}
、始点
s
{\displaystyle s}
、および
u
{\displaystyle u}
から
v
{\displaystyle v}
への辺の長さ
length
(
u
,
v
)
{\displaystyle {\text{length}}(u,v)}
を入力として受け取る。
計算終了時に、
d
(
v
)
{\displaystyle d(v)}
は始点
s
{\displaystyle s}
からの最短経路の長さ。
prev
(
v
)
{\displaystyle \operatorname {prev} (v)}
は最短経路をたどる際の前の頂点。
// 初期化
各頂点
v
∈
V
{\displaystyle v\in V}
に対し、
d
(
v
)
←
(
v
=
s
{\displaystyle d(v)\leftarrow (v=s}
ならば
0
{\displaystyle 0}
、それ以外は
∞
)
{\displaystyle \infty )}
各頂点
v
∈
V
{\displaystyle v\in V}
に対し、
prev
(
v
)
←
{\displaystyle \operatorname {prev} (v)\leftarrow }
「無し」
Q
{\displaystyle Q}
に
V
{\displaystyle V}
の頂点を全て追加
// 本計算
while (
Q
{\displaystyle Q}
が空ではない )
u
←
Q
{\displaystyle u\leftarrow Q}
から
d
(
u
)
{\displaystyle d(u)}
が最小である頂点を取り除き取り出す
for each (
u
{\displaystyle u}
からの辺がある各頂点
v
∈
V
{\displaystyle v\in V}
)
a
l
t
←
d
(
u
)
+
length
(
u
,
v
)
{\displaystyle alt\leftarrow d(u)+{\text{length}}(u,v)}
if (
d
(
v
)
>
a
l
t
{\displaystyle d(v)>alt}
)
d
(
v
)
←
a
l
t
{\displaystyle d(v)\leftarrow alt}
prev
(
v
)
←
u
{\displaystyle \operatorname {prev} (v)\leftarrow u}
なお、「
u
←
Q
{\displaystyle u\leftarrow Q}
から
d
(
u
)
{\displaystyle d(u)}
が最小である頂点を取り除き取り出す」の部分は、エドガー・ダイクストラ によるオリジナルのアルゴリズム通りに、集合内の単純探索を想定している。
「
Q
{\displaystyle Q}
の中の
d
(
u
)
{\displaystyle d(u)}
が最小な頂点
u
{\displaystyle u}
」の部分は、始点から最短経路の長さ
d
(
u
)
{\displaystyle d(u)}
で
u
{\displaystyle u}
へ到達したこととなる。以降はこの
u
{\displaystyle u}
に関する
d
(
u
)
{\displaystyle d(u)}
の値は更新されることはない。またしたがって、「for each (
u
{\displaystyle u}
からの辺がある各頂点
v
∈
V
{\displaystyle v\in V}
)」の部分は、「for each (
u
{\displaystyle u}
からの辺がある各頂点
v
∈
Q
{\displaystyle v\in Q}
)」へ変更しても同等となる。
もしも集合
Q
{\displaystyle Q}
が、
d
(
u
)
{\displaystyle d(u)}
の値に基づく優先度付きキュー の機能を併せ持っているならば、最小に当たる頂点を探索し取り出す計算量 を単純探索に比べて減らせる可能性がある。その場合、擬似コードも
Q
(
v
)
{\displaystyle Q(v)}
へ値を代入する操作を明示的に書くと分かりやすい。
// 初期化
for (
v
←
V
{\displaystyle v\leftarrow V}
)
d
(
v
)
←
(
v
=
s
{\displaystyle d(v)\leftarrow (v=s}
ならば
0
{\displaystyle 0}
、それ以外は
∞
)
{\displaystyle \infty )}
p
r
e
v
(
v
)
←
{\displaystyle prev(v)\leftarrow }
「無し」
Q
(
v
)
←
d
(
v
)
{\displaystyle Q(v)\leftarrow d(v)}
// 本計算
while (
Q
{\displaystyle Q}
が空集合ではない )
u
←
Q
{\displaystyle u\leftarrow Q}
から
d
(
u
)
{\displaystyle d(u)}
が最小である頂点
u
{\displaystyle u}
を取り除き取り出す
for each (
u
{\displaystyle u}
からの辺がある各
v
∈
V
{\displaystyle v\in V}
)
a
l
t
←
d
(
u
)
+
length
(
u
,
v
)
{\displaystyle alt\leftarrow d(u)+{\text{length}}(u,v)}
if (
d
(
v
)
>
a
l
t
{\displaystyle d(v)>alt}
)
d
(
v
)
←
a
l
t
{\displaystyle d(v)\leftarrow alt}
p
r
e
v
(
v
)
←
u
{\displaystyle prev(v)\leftarrow u}
Q
(
v
)
←
a
l
t
{\displaystyle Q(v)\leftarrow alt}
「
u
←
Q
{\displaystyle u\leftarrow Q}
から
d
(
u
)
{\displaystyle d(u)}
が最小である頂点
u
{\displaystyle u}
を取り除き取り出す」においては、
u
{\displaystyle u}
への再訪は起きないことはオリジナルと同様である(なぜならば「
Q
(
v
)
←
a
l
t
{\displaystyle Q(v)\leftarrow alt}
」の部分は代入であり、
Q
{\displaystyle Q}
の中に
v
{\displaystyle v}
の重複を起こさないためである[ 注釈 1] )。
「for each (
u
{\displaystyle u}
からの辺がある各頂点
v
∈
V
{\displaystyle v\in V}
)」の部分は「for each (
u
{\displaystyle u}
からの辺がある各頂点
v
∈
Q
{\displaystyle v\in Q}
)」でも同じ結果が得られるが、
Q
⊂
V
{\displaystyle Q\subset V}
ではあるものの、逆に計算量が増えてしまう場合もあり得ることに注意。
計算量を低減できる優先度付きキュー の例としてフィボナッチヒープ がある。また、C++ではg++拡張の__gnu_pbds::priority_queueはデフォルトでペアリングヒープ を使用しているため[ 3] 、これを使うとフィボナッチヒープと同等の計算量のコードが簡単に実装できる。 [要検証 – ノート ]
計算量 は以下の通り。
オリジナル :
O
(
V
2
)
{\displaystyle O(V^{2})}
優先度付きキュー (二分ヒープ ):
O
(
(
E
+
V
)
log
V
)
{\displaystyle O((E+V)\log {V})}
優先度付きキュー(フィボナッチヒープ ):
O
(
E
+
V
log
V
)
{\displaystyle O(E+V\log {V})}
優先度付きキューを使用した場合の計算量としては、以下の合算である。下記説明は上記擬似コードに基づき、ループ回数も考慮に入れている。
「
Q
←
V
{\displaystyle Q\leftarrow V}
」:二分ヒープもフィボナッチヒープも
O
(
V
)
{\displaystyle O(V)}
。ただし、二分ヒープは追加を V 回繰り返す実装にすると
O
(
V
log
V
)
{\displaystyle O(V\log V)}
。
「
u
←
d
(
u
)
{\displaystyle u\leftarrow d(u)}
が最小である頂点
u
∈
Q
{\displaystyle u\in Q}
」:二分ヒープもフィボナッチヒープも
O
(
V
)
{\displaystyle O(V)}
「
Q
{\displaystyle Q}
から
u
{\displaystyle u}
を取り除き取り出す」:二分ヒープもフィボナッチヒープも
O
(
V
log
V
)
{\displaystyle O(V\log V)}
「
d
(
v
)
←
d
(
u
)
+
length
(
u
,
v
)
{\displaystyle d(v)\leftarrow d(u)+{\text{length}}(u,v)}
」:二分ヒープは
O
(
E
log
V
)
{\displaystyle O(E\log V)}
、フィボナッチヒープは
O
(
E
)
{\displaystyle O(E)}
話を簡単にするため、
G
=
(
V
,
E
)
{\displaystyle G=(V,E)}
の各頂点
v
,
w
∈
V
{\displaystyle v,w\in V}
に対し、
v
{\displaystyle v}
と
w
{\displaystyle w}
を結ぶ辺は最大一つしかないとする。
v
{\displaystyle v}
と
w
{\displaystyle w}
を結ぶ辺があれば、その辺を
(
v
,
w
)
{\displaystyle (v,w)}
と書く。
最短経路問題は、ビー玉と紐を用いて解くことができる。
まずビー玉を頂点、紐を辺にするグラフを工作する。
グラフを板の上に置き、スタートの頂点にあたるビー玉だけをつまむ。
グラフが置かれている板を取り除くと、グラフは自由落下を始めるが、
スタートにあたるビー玉を持っているので、スタート地点から近いビー玉から順に落下が止まる。
ゴールにあたるビー玉が止まったとき、ゴールにあたるビー玉はスタートにあたるビー玉まで紐で一直線で結ばれている。
この直線が最短経路である。
落下が止まった頂点
v
{\displaystyle v}
に対し、
v
{\displaystyle v}
を支えている頂点
w
{\displaystyle w}
が存在する。
w
{\displaystyle w}
を
v
{\displaystyle v}
の「直前の頂点」と呼ぶことにする。
以下簡単のため、直前の頂点は1つしかないと仮定して話を進めるが、
一般の場合も同様である。
ダイクストラのアルゴリズムは、上述の解法をコンピュータ上でシミュレートしたものである。
グラフ
G
=
(
V
,
E
)
{\displaystyle G=(V,E)}
、スタートとなる頂点
s
{\displaystyle s}
、および各辺
e
{\displaystyle e}
の長さ
length
(
e
)
{\displaystyle {\text{length}}(e)}
が入力として与えられると、
このアルゴリズムは上述の解法をシミュレートし、「落下」が停止した頂点から順に、その頂点の直前の頂点が何であるかを出力する。
ゴールとなる頂点の「落下」が停止したところで、ダイクストラのアルゴリズムを止めれば、
あとはゴールから順に、直前の頂点、さらにその直前の頂点とたどっていくことで、
ゴールとスタートをつなぐ最短経路(の一つ)を求めることができる。
以上の考察から分かるように、ダイクストラ法では「次に落下が停止する頂点」(とその直前の頂点)を求めることが重要である。
そこでこれらを効率的に求める方法を説明する。
現時点で「落下」が停止している頂点の集合をHとし、
各頂点
v
∈
V
{\displaystyle v\in V}
に対して
H
∪
v
{\displaystyle H\cup {v}}
内での
v
{\displaystyle v}
と
s
{\displaystyle s}
との最短距離を
d
(
v
)
{\displaystyle d(v)}
とする(
H
∪
v
{\displaystyle H\cup {v}}
内で
s
{\displaystyle s}
から
v
{\displaystyle v}
に到達できないときは
d
(
v
)
=
inf
{\displaystyle d(v)=\inf }
とする)。
さらに、
H
{\displaystyle H}
に隣接している頂点の集合を
A
{\displaystyle A}
とする。
ここで頂点
v
{\displaystyle v}
が
H
{\displaystyle H}
に隣接しているとは、
v
{\displaystyle v}
と
H
{\displaystyle H}
内のいずれか頂点を結ぶ辺が存在することを指す。
次に落下が停止する頂点を
u
{\displaystyle u}
とし、
u
{\displaystyle u}
の直前の頂点を
w
u
∈
H
{\displaystyle w_{u}\in H}
とすると、
以下が成立することが分かる。
u
{\displaystyle u}
は
A
{\displaystyle A}
に属し、しかも
A
{\displaystyle A}
内で
d
(
u
)
{\displaystyle d(u)}
が最小になる頂点である。
s
{\displaystyle s}
から
u
{\displaystyle u}
への最短経路は
w
u
∈
H
{\displaystyle w_{u}\in H}
を通る。
そこでダイクストラ法では、「次に落下が停止する頂点」(とその直前の頂点)を効率的に求めるために、
以下の3種類のデータを管理する。
集合
H
{\displaystyle H}
と集合
A
{\displaystyle A}
各頂点
v
∈
V
{\displaystyle v\in V}
に対して
H
∪
v
{\displaystyle H\cup {v}}
内での
s
{\displaystyle s}
から
v
{\displaystyle v}
への最短距離
d
(
v
)
{\displaystyle d(v)}
。
各
v
∈
A
{\displaystyle v\in A}
に対し、
v
{\displaystyle v}
の直前の頂点
w
v
{\displaystyle w_{v}}
ダイクストラ法では、
A
{\displaystyle A}
内で
d
(
u
)
{\displaystyle d(u)}
が最小になる頂点
u
{\displaystyle u}
(
=
{\displaystyle =}
次に落下が停止する頂点)を求めて
u
{\displaystyle u}
を出力し、それにあわせて管理しているデータを更新し、そしてさらに次に落下が停止する頂点を求める、という操作を繰り返す。
そこで最後に、ダイクストラ法が管理しているデータを更新する方法を述べる。
A
{\displaystyle A}
内で
d
(
u
)
{\displaystyle d(u)}
が最小になる頂点
u
{\displaystyle u}
(
=
{\displaystyle =}
次に落下する頂点)が求まったら、まず
u
{\displaystyle u}
を
H
{\displaystyle H}
に追加する。それにあわせて
A
{\displaystyle A}
から
u
{\displaystyle u}
を除去し、
u
{\displaystyle u}
に隣接していてかつ
H
{\displaystyle H}
に属さない頂点を
A
{\displaystyle A}
に加える。
u
{\displaystyle u}
を
H
{\displaystyle H}
に追加した結果「
H
∪
v
{\displaystyle H\cup {v}}
内での
s
{\displaystyle s}
から
v
{\displaystyle v}
への最短距離」である
d
(
v
)
{\displaystyle d(v)}
が変化するのは、
u
{\displaystyle u}
と
v
{\displaystyle v}
とを結ぶ辺がある場合に限られる。
u
{\displaystyle u}
を
H
{\displaystyle H}
に追加後の「
H
∪
v
{\displaystyle H\cup {v}}
内での
s
{\displaystyle s}
から
v
{\displaystyle v}
への最短距離」は次のいずれかと一致する。
(a)
u
{\displaystyle u}
を
H
{\displaystyle H}
に追加する前の段階での
s
{\displaystyle s}
から
v
{\displaystyle v}
への最短経路
(b)
u
{\displaystyle u}
を
H
{\displaystyle H}
に追加する前の段階での
s
{\displaystyle s}
から
u
{\displaystyle u}
への最短経路を通った後で
u
{\displaystyle u}
と
v
{\displaystyle v}
を結ぶ辺を通るという経路
(a)の長さは
u
{\displaystyle u}
を
H
{\displaystyle H}
に追加する前の段階での
d
(
v
)
{\displaystyle d(v)}
に一致し、
一方(b)の長さは
u
{\displaystyle u}
を
H
{\displaystyle H}
に追加する前の段階での
d
(
u
)
{\displaystyle d(u)}
に
length
(
u
,
v
)
{\displaystyle {\text{length}}(u,v)}
を加えた長さである。
以上の考察より、
d
(
v
)
{\displaystyle d(v)}
および
w
v
{\displaystyle w_{v}}
を次のように更新すればよいことがわかる:
u
{\displaystyle u}
から
v
{\displaystyle v}
への辺が存在する各
v
{\displaystyle v}
に対し、
d
(
v
)
>
d
(
u
)
+
length
(
u
,
v
)
{\displaystyle d(v)>d(u)+{\text{length}}(u,v)}
なら、
d
(
v
)
{\displaystyle d(v)}
を「
d
(
u
)
+
length
(
u
,
v
)
{\displaystyle d(u)+{\text{length}}(u,v)}
」に更新し、さらに
w
v
{\displaystyle w_{v}}
を
u
{\displaystyle u}
に更新。
そうでなければ何もしない。
^ もしも重複を許す実装を行なうなど再訪が生じる場合には計算量が増える。しかし
d
(
u
)
{\displaystyle d(u)}
を
Q
{\displaystyle Q}
の中から取り出した値と比較し等しくない場合は再訪と判定でき防ぐことができる。