Skip to content

グラフアルゴリズム:BFS・DFS・ダイクストラ法を正しさから理解する

Prerequisite:探索アルゴリズム:二分探索の O(log n) とハッシュ表の平均 O(1)

Raw

This content is not available in your language yet.

  • グラフの表現は隣接行列と隣接リストの 2 つが基本です。領域は Θ(V2)\Theta(|V|^2)Θ(V+E)\Theta(|V|+|E|)、辺の有無の判定は O(1)O(1)O(degu)O(\deg u) で、得意なことが正反対です。
  • 幅優先探索(BFS)は、始点からの辺の本数が最小の経路を、O(V+E)O(|V|+|E|) 時間で全頂点について同時に求めます。正しさの核は「キューの中の距離はほぼ揃っている」という単調性です。
  • 深さ優先探索(DFS)は、頂点の発見時刻と完了時刻が入れ子になるという構造を作ります。この構造から、有向閉路の検出とトポロジカルソートが同じ O(V+E)O(|V|+|E|) で得られます。
  • ダイクストラ法は、辺の重みがすべて非負という仮定の下で、重み付き最短経路を求めます。非負性は「まだ確定していない頂点を経由しても近道にならない」ことを保証するために必須で、これを外すと反例が作れます。
  • 二分ヒープを優先度付きキューに使うと、ダイクストラ法は O(V+ElogV)O(|V| + |E|\log|V|) 時間で動きます。連結グラフでは O(ElogV)O(|E|\log|V|) と書けます。密グラフでは配列による O(V2)O(|V|^2) 実装のほうが速くなります。

1. 動機:つながり方だけを取り出す

Section titled “1. 動機:つながり方だけを取り出す”

1736 年、オイラーはケーニヒスベルクの町を流れる川に架かる 7 本の橋を、すべて 1 回ずつ渡って出発点に戻れるか、という問題を考えました。彼がやったことは、町の地図から距離も形も捨て、陸地を点、橋を線に置き換えることでした。残るのは「どことどこがつながっているか」だけです。この抽象化がグラフの始まりです。

同じ抽象化が現代の計算のいたるところに現れます。カーナビは交差点を頂点、道路を辺、所要時間を重みとするグラフの最短経路を解いています。パッケージマネージャは、パッケージを頂点、依存関係を有向辺とするグラフに循環がないかを調べ、インストール順序を決めます。表面上まったく違うこれらの問題が、到達可能か最少の辺数で到達するには総コストが最小の経路は何かという数個の問いに帰着します。

グラフでは計算量の測り方が一段複雑になります。ソートの入力サイズは配列の長さ nn ひとつでした(ソートアルゴリズムソート問題(Definition 2.1)[Sorting Algorithms])。グラフの入力サイズは頂点数 V|V| と辺数 E|E|2 つで決まり、しかも E|E|V1|V|-1(木)から V(V1)/2|V|(|V|-1)/2(完全グラフ)まで大きく動きます。そのため「O(V2)O(|V|^2)O(ElogV)O(|E|\log|V|) のどちらが速いか」は入力の性質を指定しないと決まりません。計算量とO記法O 記法(Definition 3.1)[Complexity and Big-O Notation] による漸近評価が実際に効いてくる場面です。

この章では、グラフの表現を決めたうえで、BFS・DFS・ダイクストラ法を動く実装証明の形で押さえます。

2. 準備:グラフの定義と 2 つの表現

Section titled “2. 準備:グラフの定義と 2 つの表現”

Definition 2.1グラフ

VV を有限集合とする。

無向グラフとは、対 G=(V,E)G=(V,E) であって E{{u,v}u,vV, uv}E \subseteq \{\, \{u,v\} \mid u,v \in V,\ u \ne v \,\} を満たすものをいう。VV の元を頂点EE の元をという。

有向グラフとは、対 G=(V,E)G=(V,E) であって EV×VE \subseteq V \times V を満たすものをいう。(u,v)E(u,v)\in Euu から vv への有向辺という。

無向グラフの頂点 uu次数 deg(u)\deg(u) を、uu を含む辺の個数とする。有向グラフでは、uu を始点とする辺の個数を出次数 deg+(u)\deg^{+}(u)uu を終点とする辺の個数を入次数 deg(u)\deg^{-}(u) とする。

さらに写像 w:ERw : E \to \mathbb{R} が与えられているとき、(G,w)(G,w)重み付きグラフといい、w(e)w(e) を辺 ee重みという。

この定義は多重辺(同じ 2 頂点を結ぶ 2 本以上の辺)と無向の自己ループを許していません。このようなグラフを単純グラフといいます。有向グラフでは (u,u)E(u,u)\in E を許すことにします。

Definition 2.2歩道・道・距離

グラフ G=(V,E)G=(V,E) において、頂点の列 W=(v0,v1,,vk)W=(v_0,v_1,\ldots,v_k) が、すべての ii について viv_ivi+1v_{i+1} が辺で結ばれている(有向グラフなら (vi,vi+1)E(v_i,v_{i+1}) \in E)とき、WWv0v_0 から vkv_k への歩道といい、kk をその長さという。

歩道 WW の頂点がすべて相異なるとき、WWという。k1k \ge 1 で、v0=vkv_0=v_k かつ v0,,vk1v_0,\ldots,v_{k-1} が相異なるとき、WW閉路という。

ss から vv への歩道が存在するとき、vvss から到達可能であるという。ss から vv への歩道の長さの最小値を δ(s,v)\delta(s,v) と書き、ss から vv への距離という。到達可能でないときは δ(s,v)=\delta(s,v)=\infty と定める。

距離を「歩道の長さの最小値」で定義しましたが、最小を与える歩道は必ず道になります。実際、歩道の中に同じ頂点 xx が 2 回現れれば、その間の部分(長さ 1 以上)を切り取ってより短い歩道が作れるので、最小性に反するからです。

以下、この章を通じて次の無向グラフを使います。

graph LR
s((s)) --- a((a))
s --- b((b))
a --- c((c))
b --- c
b --- d((d))
c --- e((e))
d --- e
この章で使う無向グラフ G0。頂点は 6 個、辺は 7 本。

Definition 2.3隣接行列と隣接リスト

V={v1,,vn}V=\{v_1,\ldots,v_n\} と番号付けされたグラフ G=(V,E)G=(V,E) に対し、n×nn \times n 行列 AA

Aij={1(vi と vj が辺で結ばれている、有向グラフなら (vi,vj)E)0(そうでない)A_{ij} = \begin{cases} 1 & (v_i \text{ と } v_j \text{ が辺で結ばれている、有向グラフなら } (v_i,v_j) \in E) \\ 0 & (\text{そうでない}) \end{cases}

で定め、GG隣接行列という。重み付きグラフでは 11 の代わりに重み ww を置き、辺がないところには \infty を置く。

各頂点 uu に対し、uu に隣接する頂点(有向グラフでは uu を始点とする辺の終点)を並べたリスト Adj[u]\mathrm{Adj}[u] の組を、GG隣接リストという。

Example 2.4G0 の 2 つの表現

上のグラフ G0G_0 を、頂点の順序を s,a,b,c,d,es,a,b,c,d,e として表します。隣接行列は

A=(011000100100100110011001001001000110)A = \begin{pmatrix} 0 & 1 & 1 & 0 & 0 & 0 \\ 1 & 0 & 0 & 1 & 0 & 0 \\ 1 & 0 & 0 & 1 & 1 & 0 \\ 0 & 1 & 1 & 0 & 0 & 1 \\ 0 & 0 & 1 & 0 & 0 & 1 \\ 0 & 0 & 0 & 1 & 1 & 0 \end{pmatrix}

です。無向グラフなので AA は対称行列になります。3636 個の成分のうち 111414 個で、辺の本数 77 のちょうど 2 倍です。

隣接リストは次のとおりです。

頂点隣接頂点次数
ssa, ba,\ b2
aas, cs,\ c2
bbs, c, ds,\ c,\ d3
cca, b, ea,\ b,\ e3
ddb, eb,\ e2
eec, dc,\ d2

次数の総和は 2+2+3+3+2+2=14=2×72+2+3+3+2+2=14=2\times 7 です。この一致は偶然ではありません。

Lemma 2.5握手補題

無向単純グラフ G=(V,E)G=(V,E) について

uVdeg(u)=2E\sum_{u \in V} \deg(u) = 2|E|

が成り立つ。有向グラフ G=(V,E)G=(V,E) については

uVdeg+(u)=uVdeg(u)=E\sum_{u \in V} \deg^{+}(u) = \sum_{u \in V} \deg^{-}(u) = |E|

が成り立つ。

Proof(Lemma 2.5)

無向の場合、集合 S={(u,e)uV, eE, ue}S = \{\,(u,e) \mid u \in V,\ e \in E,\ u \in e \,\} の元の個数を 2 通りに数えます。

頂点 uu を固定して数えると、uu を含む辺は定義により deg(u)\deg(u) 本なので、uu に対応する組は deg(u)\deg(u) 個です。uu について足し上げて S=udeg(u)|S| = \sum_{u} \deg(u) を得ます。

e={u,v}e=\{u,v\} を固定して数えると、単純グラフでは uvu \ne v なので、ee を含む組は (u,e)(u,e)(v,e)(v,e) のちょうど 2 個です。ee について足し上げて S=2E|S| = 2|E| を得ます。両者を等しいと置けば主張が従います。

有向の場合、各辺 (u,v)(u,v) は始点 uu の出次数に 1 だけ、終点 vv の入次数に 1 だけ寄与し、逆に出次数・入次数への寄与はすべてこの形なので、どちらの総和も辺の総数 E|E| に等しくなります。

Proposition 2.62 つの表現の計算量

G=(V,E)G=(V,E) を頂点数 n=Vn=|V|、辺数 m=Em=|E| のグラフとする。

(a) 隣接行列表現は Θ(n2)\Theta(n^2) 語の記憶領域を使う。与えられた u,vu,v について辺の有無の判定は O(1)O(1) 時間、uu に隣接するすべての頂点の列挙は Θ(n)\Theta(n) 時間、すべての辺の走査は Θ(n2)\Theta(n^2) 時間で行える(かつ、それより速くはできない)。

(b) 隣接リスト表現は Θ(n+m)\Theta(n+m) 語の記憶領域を使う。uu に隣接するすべての頂点の列挙は Θ(1+degu)\Theta(1+\deg u) 時間、すべての辺の走査は Θ(n+m)\Theta(n+m) 時間で行える。一方、与えられた u,vu,v について辺の有無の判定は、最悪 Θ(degu)\Theta(\deg u) 時間かかる。

Proof(Proposition 2.6)

(a) 行列は n2n^2 個の成分を持つので領域は Θ(n2)\Theta(n^2) です。辺の有無は AijA_{ij} という 1 回の添字アクセスで読めるので O(1)O(1) です。uu の隣接頂点を列挙するには、行 uunn 個の成分を全部見る必要があります。どの成分が 11 かは事前にわからないので、nn 個未満しか見ないと見落としの可能性が残り、Θ(n)\Theta(n) が必要です。全辺の走査は各行についてこれを行うので Θ(n2)\Theta(n^2) です。

(b) 領域は、頂点ごとのリストの先頭を保持する部分が Θ(n)\Theta(n)、リストの要素の総数が、無向グラフでは各辺 {u,v}\{u,v\}Adj[u]\mathrm{Adj}[u]Adj[v]\mathrm{Adj}[v] に 1 回ずつ現れるので Lemma 2.5 より udeg(u)=2m=Θ(m)\sum_u \deg(u) = 2m = \Theta(m)、有向グラフでは同じく udeg+(u)=m\sum_u \deg^{+}(u)=m です。合わせて Θ(n+m)\Theta(n+m) です。

uu の隣接頂点の列挙はリストを末尾までたどるだけで、要素数は degu\deg u 個、リストが空でも先頭を見る定数時間はかかるので Θ(1+degu)\Theta(1+\deg u) です。全辺の走査はこれを全頂点について行うので、

uVΘ(1+degu)=Θ(n+uVdegu)=Θ(n+2m)=Θ(n+m)\sum_{u \in V} \Theta(1 + \deg u) = \Theta\left(n + \sum_{u\in V}\deg u\right) = \Theta(n+2m) = \Theta(n+m)

となります(再び Lemma 2.5 を使いました)。辺の有無の判定は Adj[u]\mathrm{Adj}[u] を線形に探索するので、vv が末尾にある場合や含まれない場合に Θ(degu)\Theta(\deg u) かかります。

無向単純グラフでは mn(n1)/2m \le n(n-1)/2 です。mmnn に比例する程度の疎なグラフでは、隣接リストが Θ(n)\Theta(n) 語で済むのに対し隣接行列は Θ(n2)\Theta(n^2) 語を使い、頂点数 10510^5 の道路網なら 101010^{10} 成分となって現実的ではありません。以降は隣接リストを前提にします。逆に m=Θ(n2)m=\Theta(n^2)密なグラフでは隣接行列が有利です。

3. 幅優先探索:最短の「本数」を測る

Section titled “3. 幅優先探索:最短の「本数」を測る”

幅優先探索(breadth-first search、BFS)は、始点 ss から近い順に頂点を訪れる探索です。「近い」は辺の本数で測ります。実装は、訪問予定の頂点を先入れ先出しのキューキュー(Definition 7.1)[Fundamental Data Structures])に入れておくだけです。

from collections import deque
def bfs(adj, s):
"""adj[u] は u の隣接頂点のリスト。s は始点。"""
n = len(adj)
INF = float('inf')
dist = [INF] * n # d[v]
parent = [-1] * n # 最短路木の親
dist[s] = 0
queue = deque([s])
while queue:
u = queue.popleft() # 先頭から取り出す
for v in adj[u]: # u の隣接リストを 1 回だけ走査
if dist[v] == INF: # まだ発見していない頂点だけ
dist[v] = dist[u] + 1
parent[v] = u
queue.append(v) # 末尾に追加
return dist, parent

dist[v]INF かどうかが「未発見か既発見か」の印を兼ねている点に注意してください。値は 1 度だけ書き込まれ、以後変わりません。以下、この配列を d[v]d[v] と書きます。

d[v]d[v] が本当に距離 δ(s,v)\delta(s,v) に一致することを証明します。まず 2 つの不変式を用意します。

Lemma 3.1BFS の不変式

グラフ G=(V,E)G=(V,E)(有向でも無向でもよい)と始点 sVs \in V について上のアルゴリズムを実行する。実行中の任意の時点で次が成り立つ。

(a) すべての vVv \in V について d[v]δ(s,v)d[v] \ge \delta(s,v) である(d[v]=d[v]=\infty の場合も含む)。

(b) キューの中身を先頭から順に v1,v2,,vrv_1,v_2,\ldots,v_rr1r \ge 1)とすると

d[v1]d[v2]d[vr]d[v1]+1d[v_1] \le d[v_2] \le \cdots \le d[v_r] \le d[v_1] + 1

が成り立つ。

(c) 頂点が取り出される順に u1,u2,u_1,u_2,\ldots と並べると d[u1]d[u2]d[u_1] \le d[u_2] \le \cdots である。

Proof(Lemma 3.1)

まず、辺で結ばれた 2 頂点の距離について

u と v が辺で結ばれている(有向なら (u,v)E    δ(s,v)δ(s,u)+1u \text{ と } v \text{ が辺で結ばれている(有向なら } (u,v)\in E \text{)} \implies \delta(s,v) \le \delta(s,u)+1

が成り立つことを確かめます。δ(s,u)=\delta(s,u)=\infty なら右辺が \infty なので自明です。δ(s,u)=k<\delta(s,u)=k<\infty なら、長さ kkss から uu への歩道の末尾に辺 (u,v)(u,v) を継ぎ足すと、長さ k+1k+1ss から vv への歩道になります。δ(s,v)\delta(s,v) は歩道の長さの最小値(Definition 2.2)なので δ(s,v)k+1\delta(s,v)\le k+1 です。これを三角不等式と呼ぶことにします。

(a) 代入の回数についての帰納法で示します。初期状態では d[s]=0=δ(s,s)d[s]=0=\delta(s,s)、他の vv では d[v]=δ(s,v)d[v]=\infty\ge\delta(s,v) なので成立します。代入が起こるのは、ある uu を取り出して隣接頂点 vvd[v]d[u]+1d[v] \leftarrow d[u]+1 とするときだけです。帰納法の仮定より d[u]δ(s,u)d[u]\ge\delta(s,u) なので、三角不等式と合わせて

d[v]=d[u]+1δ(s,u)+1δ(s,v)d[v] = d[u]+1 \ge \delta(s,u)+1 \ge \delta(s,v)

となり、代入後も (a) が保たれます。

(b) キューへの操作回数についての帰納法で示します。初期状態はキューが (s)(s) だけなので、d[s]d[s]+1d[s]\le d[s]+1 で成立します。

操作は「先頭 uu を取り出す」と「取り出した uu の隣接リストを走査しながら末尾に追加する」の 2 種類しかありません。uu を取り出す直前のキューを (u,x1,,xp)(u,x_1,\ldots,x_p) とすると、帰納法の仮定より

d[u]d[x1]d[xp]d[u]+1d[u] \le d[x_1] \le \cdots \le d[x_p] \le d[u]+1

です。uu を取り出し、走査の中で新たに y1,,yqy_1,\ldots,y_qq0q\ge0)が追加された後のキューは (x1,,xp,y1,,yq)(x_1,\ldots,x_p,y_1,\ldots,y_q) であり、追加された頂点の値はすべて d[yj]=d[u]+1d[y_j]=d[u]+1 です。

p1p \ge 1 のとき、この不等式の列から d[x1]d[xp]d[u]+1=d[y1]==d[yq]d[x_1]\le\cdots\le d[x_p]\le d[u]+1=d[y_1]=\cdots=d[y_q] なので単調性が成り立ち、末尾の値は d[yq]=d[u]+1d[x1]+1d[y_q]=d[u]+1 \le d[x_1]+1d[u]d[x1]d[u]\le d[x_1] を使いました)なので先頭 +1+1 以下です。q=0q=0 のときも d[xp]d[u]+1d[x1]+1d[x_p]\le d[u]+1\le d[x_1]+1 で成立します。

p=0p=0 のとき、キューは (y1,,yq)(y_1,\ldots,y_q) で全要素の値が d[u]+1d[u]+1 に等しいので、単調性も末尾の条件も成り立ちます(q=0q=0 ならキューは空で主張は空虚に真です)。途中の状態(uu の走査の途中)でも同じ議論がそのまま通ります。

(c) ii についての帰納法で示します。ui+1u_{i+1} は取り出される瞬間にキューの先頭にいます。uiu_i を取り出した直後のキューに ui+1u_{i+1} が既に入っていた場合、uiu_i を取り出す直前のキューは (ui,,ui+1,)(u_i,\ldots,u_{i+1},\ldots) の形なので (b) より d[ui]d[ui+1]d[u_i]\le d[u_{i+1}] です。入っていなかった場合、ui+1u_{i+1} はその後キューに追加されたことになりますが、次に取り出されるのが ui+1u_{i+1} なのだから、追加が起きたのは uiu_i の走査中しかありえません。このとき d[ui+1]=d[ui]+1>d[ui]d[u_{i+1}]=d[u_i]+1 > d[u_i] です。いずれの場合も d[ui]d[ui+1]d[u_i]\le d[u_{i+1}] が成り立ちます。

Theorem 3.2BFS の正当性

G=(V,E)G=(V,E) を有向または無向のグラフ、sVs\in V とする。上のアルゴリズムは停止し、停止時にすべての vVv\in V について

d[v]=δ(s,v)d[v] = \delta(s,v)

が成り立つ。さらに vsv \ne s かつ δ(s,v)<\delta(s,v)<\infty ならば、v, parent[v], parent[parent[v]], v,\ \mathrm{parent}[v],\ \mathrm{parent}[\mathrm{parent}[v]],\ \ldots とたどって得られる列を逆向きに読んだものは、ss から vv への長さ δ(s,v)\delta(s,v) の道である。

Proof(Theorem 3.2)

停止性。 d[v]d[v] に値が入るのは 1 回だけ(代入の条件が dist[v] == INF だから)で、キューへの追加はその代入と同時にしか起こりません。よって追加は高々 V|V| 回、取り出しも高々 V|V| 回で、ループは有限回で終わります。

到達不能な頂点。 代入は「既に dd が定まった頂点 uu から辺をたどった先」に対してしか起こらないので、d[v]<d[v]<\infty となった頂点は ss から到達可能です(代入回数についての帰納法。d[s]=0d[s]=0ss は自明に到達可能)。対偶をとれば、到達不能な vv では最後まで d[v]==δ(s,v)d[v]=\infty=\delta(s,v) です。

到達可能な頂点。 d[v]δ(s,v)d[v]\ne\delta(s,v) となる到達可能な頂点が存在したと仮定し、その中で δ(s,v)\delta(s,v) が最小のものを vv とします。Lemma 3.1 (a) より d[v]δ(s,v)d[v]\ge\delta(s,v) なので、実際には d[v]>δ(s,v)d[v]>\delta(s,v) です。

d[s]=0=δ(s,s)d[s]=0=\delta(s,s) なので vsv\ne s であり、k:=δ(s,v)1k:=\delta(s,v)\ge1 です。長さ kkss から vv への道を取り、vv の 1 つ手前の頂点を uu とします。この道の uu までの部分は長さ k1k-1 の歩道なので δ(s,u)k1\delta(s,u)\le k-1 であり、三角不等式から k=δ(s,v)δ(s,u)+1k=\delta(s,v)\le\delta(s,u)+1、すなわち δ(s,u)k1\delta(s,u)\ge k-1 です。よって δ(s,u)=k1<k\delta(s,u)=k-1<k となり、vv の最小性から d[u]=δ(s,u)=k1d[u]=\delta(s,u)=k-1 です。特に d[u]<d[u]<\infty なので uu はキューに入り、停止性からいつか取り出されます。

uu が取り出された瞬間に、辺をたどって vv が調べられます。このとき d[v]d[v]INF なら、その場で d[v]d[u]+1=kd[v]\leftarrow d[u]+1=k となり d[v]>kd[v]>k に反します。d[v]d[v] が既に定まっていたなら、その値はある頂点 xx が取り出されたときに d[v]=d[x]+1d[v]=d[x]+1 として書き込まれたものであり、xxuu と同時か uu より前に取り出されています。Lemma 3.1 (c) より d[x]d[u]d[x]\le d[u] なので

d[v]=d[x]+1d[u]+1=kd[v] = d[x]+1 \le d[u]+1 = k

となり、やはり d[v]>kd[v]>k に反します。どちらの場合も矛盾するので、そのような vv は存在しません。

親の列。 parent[v]=u\mathrm{parent}[v]=u と書き込まれるのは d[v]=d[u]+1d[v]=d[u]+1 と同時なので、d[parent[v]]=d[v]1d[\mathrm{parent}[v]]=d[v]-1 が常に成り立ちます。vv から親をたどると dd の値が 1 ずつ減るので、δ(s,v)\delta(s,v) 回で d=0d=0 の頂点、すなわち ss に到達します(d[x]=0d[x]=0 となるのは x=sx=s だけです)。隣り合う 2 頂点は辺で結ばれているので、この列を逆向きに読めば長さ δ(s,v)\delta(s,v)ss-vv 歩道であり、dd の値が相異なるので頂点も相異なり、道になっています。

Proposition 3.3BFS の計算量

隣接リスト表現の下で、BFS は O(V+E)O(|V|+|E|) 時間、O(V)O(|V|) の追加領域で動作する。隣接行列表現の下では Θ(V2)\Theta(|V|^2) 時間かかる。

Proof(Proposition 3.3)

配列 ddparent\mathrm{parent} の初期化に Θ(V)\Theta(|V|) かかります。Theorem 3.2 の停止性の議論より、各頂点はキューに高々 1 回しか入らないので、appendpopleft は合わせて O(V)O(|V|) 回で、deque ではそれぞれ O(1)O(1) 時間です。

取り出された頂点 uu について、隣接リスト Adj[u]\mathrm{Adj}[u] を 1 回だけ走査します。1 つの頂点は高々 1 回しか取り出されないので、走査の総費用は Proposition 2.6 (b) より uΘ(1+degu)=Θ(V+E)\sum_{u}\Theta(1+\deg u)=\Theta(|V|+|E|) で抑えられます。以上を足して O(V+E)O(|V|+|E|) です。追加領域はキューと 2 本の配列で O(V)O(|V|) です。

隣接行列表現では、頂点 uu の隣接頂点を列挙するのに Proposition 2.6 (a) より Θ(V)\Theta(|V|) かかり、これを取り出された頂点すべてについて行うので Θ(V2)\Theta(|V|^2) になります。

Example 3.4G0 に対する BFS の実行

G0G_0 の隣接リスト(Example 2.4)を上の表の順で与え、ss から BFS を実行します。キューの状態を、取り出しの直後・走査の直後の順に追います。

取り出す頂点走査で新たに発見走査後のキュー確定した dd
(s)(s)d[s]=0d[s]=0
ssa, ba,\ b(a,b)(a,b)d[a]=1, d[b]=1d[a]=1,\ d[b]=1
aacc(b,c)(b,c)d[c]=2d[c]=2
bbdd(c,d)(c,d)d[d]=2d[d]=2
ccee(d,e)(d,e)d[e]=3d[e]=3
ddなし(e)(e)
eeなし()()

結果は d[s]=0d[s]=0d[a]=d[b]=1d[a]=d[b]=1d[c]=d[d]=2d[c]=d[d]=2d[e]=3d[e]=3 です。実際、ss から ee への道は s,a,c,es,a,c,es,b,d,es,b,d,es,b,c,es,b,c,e がありますが、いずれも長さ 3 で、これより短い道はありません(長さ 2 の道があるとすれば ss の隣接頂点 aabbee と隣接している必要がありますが、Adj[e]={c,d}\mathrm{Adj}[e]=\{c,d\} なのでそうなっていません)。

キューの dd 値は順に (1,1),(1,2),(2,2),(2,3)(1,1),(1,2),(2,2),(2,3) で、いずれも Lemma 3.1 (b) の「単調で、末尾は先頭 +1+1 以下」を満たしています。

BFS は、辺の重みがすべて等しい場合の最短経路アルゴリズムです。重みが等しくないときは後半のダイクストラ法が必要になります。探索アルゴリズム二分探索(Theorem 3.2)[探索アルゴリズム] が「配列の中を探す」ものだったのに対し、グラフ探索は「つながりをたどって探す」ものだと対比すると、位置づけがはっきりします。

4. 深さ優先探索:時刻の入れ子構造

Section titled “4. 深さ優先探索:時刻の入れ子構造”

深さ優先探索(depth-first search、DFS)は、行けるところまで行って、行き止まりになったら 1 歩戻る探索です。再帰で書くのが自然です。頂点に(未発見)・灰色(発見済みで走査中)・(走査完了)の 3 色を付け、色が変わる時刻を記録します。

WHITE, GRAY, BLACK = 0, 1, 2
def dfs(adj):
"""adj[u] は u の隣接頂点のリスト。全頂点を始点候補として探索する。"""
n = len(adj)
color = [WHITE] * n
disc = [0] * n # 発見時刻 d[u]
fin = [0] * n # 完了時刻 f[u]
back_edges = []
time = 0
def visit(u):
nonlocal time
color[u] = GRAY
time += 1
disc[u] = time
for v in adj[u]:
if color[v] == WHITE:
visit(v) # v は u の子になる
elif color[v] == GRAY:
back_edges.append((u, v)) # 後退辺
color[u] = BLACK
time += 1
fin[u] = time
for u in range(n):
if color[u] == WHITE:
visit(u)
return disc, fin, back_edges

visit(v)visit(u) の中から呼ばれたとき、vvuuと呼びます。こうして全頂点上に森(DFS 森)ができます。時刻は色が変わるたびに 1 進むので 2V2|V| で終わり、d[u]<f[u]d[u]<f[u] が常に成り立ちます。

DFS の力は、再帰呼び出しの入れ子構造がそのまま頂点の時刻の入れ子構造になることにあります。

Lemma 4.1灰色頂点の構造

上の DFS の実行中の任意の時点について、次が成り立つ。

(a) そのとき灰色である頂点を、再帰スタックの底から順に x1,x2,,xkx_1,x_2,\ldots,x_k とすると、各 ii について xi+1x_{i+1}xix_i の子である。特に x1,x2,,xkx_1,x_2,\ldots,x_kGG の歩道であり、xkx_k は現在隣接リストを走査中の頂点である。

(b) 頂点 vv が灰色である間に発見された頂点 zzzvz \ne v)は、DFS 森において vv の子孫であり、f[z]<f[v]f[z]<f[v] を満たす。

Proof(Lemma 4.1)

(a) 実行中に起きることは「visit(u) の呼び出し」と「visit(u) からの復帰」の 2 つだけなので、これらについての帰納法で示します。最初は灰色の頂点がなく主張は空虚に真です。

visit(v) が呼ばれるのは 2 か所です。1 つは最外側のループからで、このとき灰色の頂点は 1 つもないので、新しい列は (v)(v) の 1 個だけになり主張は成り立ちます。もう 1 つは visit(x_k) の隣接リスト走査の中で vv が白だった場合で、このとき vvxkx_k の子であり、vv が灰色になって列は x1,,xk,vx_1,\ldots,x_k,v になります。帰納法の仮定と合わせて (a) が保たれます。visit(x_k) から復帰するときは xkx_k が黒になって列の末尾が落ちるだけなので、主張は保たれます。

xi+1x_{i+1}xix_i の子であるということは、xi+1x_{i+1}xix_i の隣接リストに現れたということなので、xix_ixi+1x_{i+1} は辺で結ばれています。よって列は歩道です。

(b) zz が発見された瞬間を考えます。(a) より、そのとき灰色の列は x1,,xkx_1,\ldots,x_k の形で、zz は走査中の頂点 xkx_k の子として発見されます。いま vv が灰色なのだから v=xiv=x_i となる ii があり、(a) より xkx_kxix_i の子孫(または xix_i 自身)なので、その子である zzvv の子孫です。

子孫であることは、visit(z) の呼び出しが visit(v) の実行中に(入れ子になって)行われることを意味します。再帰呼び出しは内側が先に終わるので、visit(z) の復帰は visit(v) の復帰より前に起こり、f[z]<f[v]f[z]<f[v] です。

4.3. 閉路検出とトポロジカルソート

Section titled “4.3. 閉路検出とトポロジカルソート”

Theorem 4.2有向閉路の検出

G=(V,E)G=(V,E) を有向グラフとする。次の 2 つは同値である。

(i) GG は有向閉路を含む。

(ii) 上の DFS の実行中に、後退辺(走査時に灰色である頂点へ向かう辺)が少なくとも 1 本記録される。

したがって DFS により、GG が有向閉路を持つかどうかを O(V+E)O(|V|+|E|) 時間で判定できる。

Proof(Theorem 4.2)

(ii) \Rightarrow (i)。 後退辺 (u,v)(u,v) が記録されたとします。これは uu の走査中、つまり uu が灰色のときに、vv も灰色であったということです。Lemma 4.1 (a) より、その時点の灰色頂点は x1,,xkx_1,\ldots,x_k という歩道をなし、u=xku=x_k(走査中の頂点)、v=xiv=x_i(ある iki \le k)です。よって xi,xi+1,,xkx_i,x_{i+1},\ldots,x_kvv から uu への歩道であり、これに辺 (u,v)(u,v) を継ぎ足すと vv に戻る長さ 11 以上の閉じた歩道になります。灰色頂点は相異なるので、これは有向閉路です(i=ki=k、すなわち u=vu=v のときは自己ループで、これも閉路です)。

(i) \Rightarrow (ii)。 GG が有向閉路 C:v0v1vm1v0C: v_0 \to v_1 \to \cdots \to v_{m-1} \to v_0m1m\ge1、頂点は相異なる)を含むとします。CC の頂点のうち DFS が最初に発見するものを、番号を付け替えて v0v_0 とします。

まず、j=1,,m1j=1,\ldots,m-1 について「vjv_jv0v_0 が灰色である間に発見される」ことを jj についての帰納法で示します。

j=1j=1 のとき。v0v_0 の走査中に辺 (v0,v1)(v_0,v_1) が調べられます。その瞬間 v1v_1 が白なら、そこで発見されるので主張が成り立ちます。白でないなら、v1v_1 は既に発見済みですが、v0v_0 が最初に発見された頂点なので発見時刻は d[v0]d[v_0] より後、かついま考えている瞬間(f[v0]f[v_0] より前)よりは前です。つまり v0v_0 が灰色である間に発見されています。

jj から j+1j+11jm21\le j\le m-2)へ。帰納法の仮定より vjv_jv0v_0 が灰色の間に発見され、Lemma 4.1 (b) より f[vj]<f[v0]f[v_j]<f[v_0] です。したがって vjv_j の走査は v0v_0 の灰色の期間の内側で行われます。その走査中に辺 (vj,vj+1)(v_j,v_{j+1}) が調べられ、j=1j=1 の場合とまったく同じ場合分けにより、vj+1v_{j+1}v0v_0 が灰色である間に発見されます(vj+1v_{j+1}CC の頂点なので、d[v0]d[v_0] の時点では白でした)。

これで vm1v_{m-1}v0v_0 の灰色期間中に発見されることがわかりました。再び Lemma 4.1 (b) より f[vm1]<f[v0]f[v_{m-1}]<f[v_0] なので、vm1v_{m-1} の走査は v0v_0 がまだ灰色のうちに行われます。その走査で辺 (vm1,v0)(v_{m-1},v_0) が調べられ、v0v_0 は灰色なので、これが後退辺として記録されます(m=1m=1 のときは v0v_0 の自己ループが v0v_0 の走査中に後退辺として記録されます)。

計算量。 各頂点について visit は高々 1 回呼ばれ(白のときだけ呼ばれ、直後に灰色になるため)、その中で隣接リストを 1 回走査するので、Proposition 2.6 (b) より総費用は O(V+E)O(|V|+|E|) です。

Remark 4.3

無向グラフでこの判定をそのまま使うことはできません。辺 {u,v}\{u,v\} をたどって vv に来たあと、vv の走査で uu を見ると uu は灰色なので、閉路がなくても必ず後退辺が出てしまうからです。無向グラフでは「親へ戻る辺を除いて、灰色の頂点へ向かう辺があるか」で判定します。この修正版は正しく、証明は上とほぼ同じです。

Example 4.4G0 に対する DFS の実行

G0G_0Example 2.4)の隣接リストを表の順で与え、ss から DFS を始めます。sacbdes \to a \to c \to b \to d \to e の順に深く潜り、ee で行き止まりになって戻ります。時刻は次のようになります。

頂点発見時刻 dd完了時刻 ff
ss112
aa211ss
cc310aa
bb49cc
dd58bb
ee67dd

区間 [d[u],f[u]][d[u],f[u]][1,12][2,11][3,10][4,9][5,8][6,7][1,12] \supset [2,11] \supset [3,10] \supset [4,9] \supset [5,8] \supset [6,7] と完全に入れ子になっています。これが Lemma 4.1 の言う構造です。

親への辺を除くと、後退辺は (b,s)(b,s)bb の走査時に ss は灰色)と (e,c)(e,c)ee の走査時に cc は灰色)の 2 本です。それぞれ閉路 s,a,c,b,ss,a,c,b,sc,b,d,e,cc,b,d,e,c を与えます。G0G_0 は連結で EV+1=76+1=2|E|-|V|+1 = 7-6+1 = 2 なので、独立な閉路の本数が 2 であることとも合っています。

BFS(Example 3.4)が ss の近くから順に訪れたのに対し、DFS は a,c,b,d,ea,c,b,d,e と一直線に潜ります。同じグラフでも探索木の形はまったく違います。

Remark 4.5

有向非巡回グラフ(DAG、有向閉路を持たない有向グラフ)では、完了時刻 ff の降順に頂点を並べると、すべての辺が前から後ろへ向く順序(トポロジカル順序)になります。

理由は次のとおりです。辺 (u,v)(u,v) を任意に取り、uu の走査でこの辺を調べる瞬間の vv の色で場合分けします。vv が白なら vvuu の子になり Lemma 4.1 (b) から f[v]<f[u]f[v]<f[u] です。vv が灰色なら (u,v)(u,v) は後退辺なので Theorem 4.2 より GG に有向閉路が存在し、DAG であることに反します。vv が黒なら f[v]f[v] は既に確定していて f[u]f[u] はまだ確定していないので f[v]<f[u]f[v]<f[u] です。いずれの場合も f[v]<f[u]f[v]<f[u] となり、ff の降順では uuvv より先に来ます。

この順序は、依存関係の解決(ビルド順序、履修順序)や、DAG 上での漸化式の計算順序を与えます。後者は 動的計画法(Dynamic Programming) の基礎になる考え方です(部分問題族と依存グラフ(Definition 3.1)[動的計画法])。

5. 重み付き最短経路とダイクストラ法

Section titled “5. 重み付き最短経路とダイクストラ法”

カーナビが求めているのは「交差点を何回通るか」ではなく「所要時間の合計」です。辺に重みが付いたグラフでは、距離の定義を取り替える必要があります。

Definition 5.1重み付き最短経路

G=(V,E)G=(V,E) を有向グラフ、w:ER0w:E\to\mathbb{R}_{\ge0} を非負の重み関数とする。歩道 W=(v0,,vk)W=(v_0,\ldots,v_k)重み

w(W)=i=0k1w(vi,vi+1)w(W) = \sum_{i=0}^{k-1} w(v_i, v_{i+1})

で定める(k=0k=0 のときは w(W)=0w(W)=0)。s,vVs,v\in V に対し

δw(s,v)=min{w(W)W は s から v への歩道}\delta_w(s,v) = \min \{\, w(W) \mid W \text{ は } s \text{ から } v \text{ への歩道} \,\}

ss から vv への最短距離といい、vvss から到達不能なときは δw(s,v)=\delta_w(s,v)=\infty と定める。最小値を与える歩道を最短路という。

重みが非負なので、この最小値は必ず存在します。歩道に同じ頂点が 2 回現れれば、その間の閉じた部分の重みは非負なので、取り除いても重みは増えません。よって最小値の候補は道だけで足り、道は有限個しかないからです。

Lemma 5.2最短路の最適部分構造

ww を非負の重み関数とする。P=(v0,v1,,vk)P=(v_0,v_1,\ldots,v_k)v0v_0 から vkv_k への最短路ならば、任意の 0ijk0\le i\le j\le k について、部分列 Pij=(vi,,vj)P_{ij}=(v_i,\ldots,v_j)viv_i から vjv_j への最短路である。特に δw(v0,vj)=w(P0j)\delta_w(v_0,v_j)=w(P_{0j}) が成り立つ。

Proof(Lemma 5.2)

重みの定義から w(P)=w(P0i)+w(Pij)+w(Pjk)w(P)=w(P_{0i})+w(P_{ij})+w(P_{jk}) と分解できます。

PijP_{ij} が最短でないと仮定すると、viv_i から vjv_j への歩道 QQw(Q)<w(Pij)w(Q)<w(P_{ij}) となるものが存在します。P0iP_{0i}QQPjkP_{jk} をこの順につないだものは v0v_0 から vkv_k への歩道 PP' であり、

w(P)=w(P0i)+w(Q)+w(Pjk)<w(P0i)+w(Pij)+w(Pjk)=w(P)w(P') = w(P_{0i}) + w(Q) + w(P_{jk}) < w(P_{0i}) + w(P_{ij}) + w(P_{jk}) = w(P)

となります。Definition 5.1 では最小を歩道の全体で取っているので、これは PP が最短であることに反します。よって PijP_{ij} は最短路です。i=0i=0jj を任意として δw(v0,vj)=w(P0j)\delta_w(v_0,v_j)=w(P_{0j}) を得ます。

この補題は「最短路の一部分を切り出しても最短路である」と言っています。だからこそ、最短距離を短い順に 1 つずつ確定していく戦略が成り立ちます。同じ構造は 動的計画法(Dynamic Programming) の出発点でもあります(最適部分構造(Definition 5.1)[動的計画法])。

ダイクストラ法(Dijkstra, 1959)の考え方はこうです。各頂点 vv に暫定値 dist[v](今わかっている最良の ss-vv 歩道の重み)を持たせ、暫定値が最小の未確定頂点を選んでその値を確定させ、そこから出る辺で隣の暫定値を改善する(緩和する)。これを繰り返します。最小の頂点を取り出す操作は優先度付きキューがちょうど提供します。

Python の heapq は要素の値を後から下げる操作(decrease-key)を持たないので、改善のたびに新しい要素を押し込み、古い要素は取り出したときに捨てる遅延削除の書き方をします。

import heapq
def dijkstra(adj, s):
"""adj[u] は (v, w) のリスト(u から v への重み w の辺)。すべての w は 0 以上。"""
n = len(adj)
INF = float('inf')
dist = [INF] * n
dist[s] = 0
pq = [(0, s)] # (暫定値, 頂点) の最小ヒープ
while pq:
d, u = heapq.heappop(pq)
if d > dist[u]:
continue # 古い要素なので捨てる
for v, w in adj[u]: # u から出る辺を緩和する
nd = d + w
if nd < dist[v]:
dist[v] = nd
heapq.heappush(pq, (nd, v))
return dist

以下、取り出した要素が d > dist[u] で捨てられなかったとき、「uu処理された」ということにします。

S : すでに正しく確定した頂点の集合最短路の前半w(x, y)残り(重みは 0 以上)s始点x確定済みyS の外の最初の頂点uu : いま取り出した頂点s から u への最短路は必ず S の境界を横切る。その最初の横断辺が x から y への辺。
ダイクストラ法の正当性の骨格。確定済み集合 S の外に初めて出る辺 (x, y) に注目する。

Theorem 5.3ダイクストラ法の正当性

G=(V,E)G=(V,E) を有向グラフ、w:ERw:E\to\mathbb{R}すべての辺 ee について w(e)0w(e)\ge0 を満たす重み関数、sVs\in V とする。このとき上のアルゴリズムは停止し、停止時にすべての vVv\in V について

dist[v]=δw(s,v)\texttt{dist}[v] = \delta_w(s,v)

が成り立つ。

Proof(Theorem 5.3)

手順 1(下界)。 実行中の任意の時点で、すべての vv について dist[v]δw(s,v)\texttt{dist}[v]\ge\delta_w(s,v) が成り立ちます。代入回数についての帰納法で示します。初期状態は dist[s]=0=δw(s,s)\texttt{dist}[s]=0=\delta_w(s,s)(重みが非負なので ss に戻る歩道の重みは 0 以上)と dist[v]=\texttt{dist}[v]=\infty で成立します。代入は uu の処理中に dist[v]d+w(u,v)\texttt{dist}[v]\leftarrow d+w(u,v)(ここで d=dist[u]d=\texttt{dist}[u])という形でしか起こらず、帰納法の仮定から dδw(s,u)d\ge\delta_w(s,u) なので

dist[v]=d+w(u,v)δw(s,u)+w(u,v)δw(s,v)\texttt{dist}[v] = d + w(u,v) \ge \delta_w(s,u)+w(u,v) \ge \delta_w(s,v)

です。最後の不等号は、ss から uu への重み δw(s,u)\delta_w(s,u) の歩道に辺 (u,v)(u,v) を継ぎ足せば ss から vv への歩道になることから従います。また、この式から dist[v]<\texttt{dist}[v]<\infty ならばそれはある ss-vv 歩道の重みであることもわかります。

手順 2(取り出す鍵は減らない、各頂点は高々 1 回処理される)。 取り出される鍵(ヒープの最小値)の列は非減少です。なぜなら、鍵 dd の要素を取り出したあとに押し込まれる鍵は d+w(u,v)dd+w(u,v)\ge dw0w\ge0)であり、ヒープに残っている他の要素の鍵も dd 以上だからです。

いま uu が鍵 d1d_1 で処理されたとします。このとき dist[u]=d1\texttt{dist}[u]=d_1 です。その後 dist[u]\texttt{dist}[u] が更新されるには、ある頂点 xx の処理中に d2+w(x,u)<dist[u]=d1d_2+w(x,u)<\texttt{dist}[u]=d_1 となる必要がありますが、d2d_2 はその時点で取り出された鍵なので d2d1d_2 \ge d_1、したがって d2+w(x,u)d1d_2+w(x,u)\ge d_1 となり更新は起こりません。よって dist[u]\texttt{dist}[u]uu の処理後に変化せず、以後 uu の要素を取り出しても鍵は d1d_1 以上で d > dist[u] により捨てられます(鍵がちょうど d1d_1 の要素は、uu の処理時に取り出されたもの以外に押し込まれていません。押し込みは dist[u]\texttt{dist}[u] の狭義の減少と同時にしか起こらないからです)。ゆえに各頂点は高々 1 回しか処理されません。

手順 3(待機中の要素)。 実行中の任意の時点で、dist[y]<\texttt{dist}[y]<\infty かつ yy が未処理ならば、ヒープには鍵 dist[y]\texttt{dist}[y]yy の要素が入っています。実際、dist[y]\texttt{dist}[y] が最後に更新されたときに鍵 dist[y]\texttt{dist}[y] の要素が押し込まれており、もしそれが既に取り出されていたなら、取り出し時に鍵と dist[y]\texttt{dist}[y] が一致していたので yy は処理されていたはずで、未処理という仮定に反するからです。

手順 4(処理時の値は正しい)。 処理される頂点を処理順に u1,u2,u_1,u_2,\ldots とします。「uiu_i が処理される時点で dist[ui]=δw(s,ui)\texttt{dist}[u_i]=\delta_w(s,u_i)」がすべての ii で成り立つことを、最小反例を取って示します。そうでない ii が存在するとし、最小のものを取って u:=uiu:=u_id:=dist[u]d:=\texttt{dist}[u] と置きます。手順 1 より d>δw(s,u)d>\delta_w(s,u) です。

u1=su_1=s であり dist[s]=0=δw(s,s)\texttt{dist}[s]=0=\delta_w(s,s) なので usu\ne s です。また d>δw(s,u)d>\delta_w(s,u) から δw(s,u)<\delta_w(s,u)<\infty、つまり uuss から到達可能です。そこで ss から uu への最短路 PP を 1 つ取ります。

S={u1,,ui1}S=\{u_1,\ldots,u_{i-1}\}uu が処理される直前までに処理された頂点の集合)と置きます。s=u1Ss=u_1\in SuSu\notin S なので、PP 上には SS に属さない頂点があります。その最初のものを yyPP 上での yy の直前の頂点を xx とすると、xSx\in S です(図を参照)。

xxuu より前に処理されているので、ii の最小性より dist[x]=δw(s,x)\texttt{dist}[x]=\delta_w(s,x)xx の処理時に成り立っていました。その処理の中で辺 (x,y)(x,y) が緩和されるので、緩和の直後に

dist[y]δw(s,x)+w(x,y)\texttt{dist}[y] \le \delta_w(s,x) + w(x,y)

となります。ここで Lemma 5.2PP に適用すると、PPxx までの部分は最短路なので δw(s,x)=w(Psx)\delta_w(s,x)=w(P_{s\to x}) であり、yy までの部分も最短路なので δw(s,y)=w(Psx)+w(x,y)=δw(s,x)+w(x,y)\delta_w(s,y)=w(P_{s\to x})+w(x,y)=\delta_w(s,x)+w(x,y) です。したがって dist[y]δw(s,y)\texttt{dist}[y]\le\delta_w(s,y) となり、手順 1 の下界と合わせて dist[y]=δw(s,y)\texttt{dist}[y]=\delta_w(s,y) です。dist\texttt{dist} は減る一方なので、この等式は uu が取り出される時点まで保たれます。

ySy\notin S なので yy は未処理です。手順 3 より、uu が取り出される瞬間、ヒープには鍵 δw(s,y)\delta_w(s,y)yy の要素が入っています。uu の要素はそのヒープの最小値として取り出されたのだから

dδw(s,y).d \le \delta_w(s,y).

一方、yyPP 上の頂点なので Lemma 5.2 より δw(s,u)=δw(s,y)+w(Pyu)\delta_w(s,u)=\delta_w(s,y)+w(P_{y\to u}) であり、重みが非負なので w(Pyu)0w(P_{y\to u})\ge0、したがって δw(s,y)δw(s,u)\delta_w(s,y)\le\delta_w(s,u) です。2 つを合わせると dδw(s,u)d\le\delta_w(s,u) となり、d>δw(s,u)d>\delta_w(s,u) に矛盾します。よって反例は存在しません。

手順 5(停止と、到達可能・不能な頂点)。 手順 2 より各頂点は高々 1 回処理され、辺 (u,v)(u,v) の緩和は uu の処理中にしか起こらないので、緩和は高々 E|E| 回です。押し込みは緩和が成功したときだけ起こるので高々 E+1|E|+1 回(初期の 1 回を含む)、したがって取り出しも高々 E+1|E|+1 回で、ループは停止します。

停止時にはヒープが空なので、手順 3 より dist[v]<\texttt{dist}[v]<\infty となるすべての vv は処理済みで、手順 4 と手順 2(処理後は値が変わらない)より dist[v]=δw(s,v)\texttt{dist}[v]=\delta_w(s,v) です。逆に vvss から到達可能なら dist[v]<\texttt{dist}[v]<\infty であることを、最短路 s=p0,p1,,pk=vs=p_0,p_1,\ldots,p_k=v に沿った帰納法で示します。p0=sp_0=sdist[s]=0<\texttt{dist}[s]=0<\infty で処理されます。pjp_j が処理されれば、その処理中に辺 (pj,pj+1)(p_j,p_{j+1}) が緩和されて dist[pj+1]<\texttt{dist}[p_{j+1}]<\infty となり、手順 3 とヒープが空になることから pj+1p_{j+1} も処理されます。よって vv も処理され dist[v]<\texttt{dist}[v]<\infty です。対偶より、到達不能な vvdist[v]==δw(s,v)\texttt{dist}[v]=\infty=\delta_w(s,v) のままです。

Proposition 5.4ダイクストラ法の計算量

隣接リスト表現と二分ヒープによる優先度付きキューを用いると、上の実装は O(V+ElogV)O(|V| + |E|\log|V|) 時間、O(V+E)O(|V|+|E|) 領域で動作する。特に V2|V|\ge2 でグラフが連結(有向グラフなら ss から全頂点に到達可能)ならば EV1|E|\ge|V|-1 なので、これは O(ElogV)O(|E|\log|V|) と書ける。

Proof(Proposition 5.4)

Theorem 5.3 の手順 5 より、押し込みは高々 E+1|E|+1 回、取り出しも高々 E+1|E|+1 回です。したがってヒープに入る要素数は常に E+1|E|+1 以下で、二分ヒープの 1 回の操作は O(log(E+1))O(\log(|E|+1)) 時間です。単純な有向グラフでは EV(V1)<V2|E|\le|V|(|V|-1) < |V|^2 なので

log(E+1)log(V2+1)=O(logV)\log(|E|+1) \le \log(|V|^2+1) = O(\log|V|)

です。よってヒープ操作の総費用は O(ElogV)O(|E|\log|V|) です。

ヒープ以外の仕事は、配列 dist\texttt{dist} の初期化に Θ(V)\Theta(|V|)、処理された頂点の隣接リストの走査に Proposition 2.6 (b) より O(V+E)O(|V|+|E|) です。合計して O(V+ElogV)O(|V|+|E|\log|V|) を得ます。領域はヒープが O(E)O(|E|)、配列が O(V)O(|V|) です。

連結性の仮定の下では EV1|E|\ge|V|-1 なので V=O(E)|V| = O(|E|) であり、V2E2ElogV|V| \le 2|E| \le 2|E|\log|V|V2|V|\ge2 より EV1V/2|E|\ge|V|-1\ge|V|/2 かつ logV1\log|V|\ge1)から O(V+ElogV)=O(ElogV)O(|V|+|E|\log|V|)=O(|E|\log|V|) となります。

Example 5.5重み付きグラフでの実行を最後まで追う

次の有向グラフ G1G_1 を考えます。

graph LR
s((s)) -->|4| a((a))
s -->|1| b((b))
b -->|2| a
b -->|6| c((c))
a -->|3| c
a -->|6| d((d))
c -->|1| d
重み付き有向グラフ G1。辺のラベルが重み。

ss を始点として実行します。ヒープの中身は鍵の昇順に書きます。

取り出した要素判定緩和の内訳緩和後のヒープ
初期化(0,s)(0,s)
(0,s)(0,s)処理dist[a]4\texttt{dist}[a]\leftarrow4dist[b]1\texttt{dist}[b]\leftarrow1(1,b),(4,a)(1,b),(4,a)
(1,b)(1,b)処理1+2=3<41+2=3<4dist[a]3\texttt{dist}[a]\leftarrow31+6=7<1+6=7<\inftydist[c]7\texttt{dist}[c]\leftarrow7(3,a),(4,a),(7,c)(3,a),(4,a),(7,c)
(3,a)(3,a)処理3+3=6<73+3=6<7dist[c]6\texttt{dist}[c]\leftarrow63+6=9<3+6=9<\inftydist[d]9\texttt{dist}[d]\leftarrow9(4,a),(6,c),(7,c),(9,d)(4,a),(6,c),(7,c),(9,d)
(4,a)(4,a)破棄4>dist[a]=34>\texttt{dist}[a]=3(6,c),(7,c),(9,d)(6,c),(7,c),(9,d)
(6,c)(6,c)処理6+1=7<96+1=7<9dist[d]7\texttt{dist}[d]\leftarrow7(7,c),(7,d),(9,d)(7,c),(7,d),(9,d)
(7,c)(7,c)破棄7>dist[c]=67>\texttt{dist}[c]=6(7,d),(9,d)(7,d),(9,d)
(7,d)(7,d)処理dd から出る辺はない(9,d)(9,d)
(9,d)(9,d)破棄9>dist[d]=79>\texttt{dist}[d]=7

結果は dist=(s:0, a:3, b:1, c:6, d:7)\texttt{dist}=(s:0,\ a:3,\ b:1,\ c:6,\ d:7) です。答え合わせをします。ss から dd への道は s,a,ds,a,d4+6=104+6=10)、s,b,a,ds,b,a,d1+2+6=91+2+6=9)、s,a,c,ds,a,c,d4+3+1=84+3+1=8)、s,b,c,ds,b,c,d1+6+1=81+6+1=8)、s,b,a,c,ds,b,a,c,d1+2+3+1=71+2+3+1=7)の 5 本で、最小は 7 です。確かに一致しています。

aa の暫定値が 44 から 33 へ下がり、古い要素 (4,a)(4,a) が後で破棄されている点が、遅延削除の実装の要点です。処理された頂点は s,b,a,c,ds,b,a,c,d の順で、これは最短距離 0,1,3,6,70,1,3,6,7 の昇順になっています。Theorem 5.3 の手順 2 が言っていたのはこのことです。

Example 5.6負の重みがあると正しくない

V={s,a,b}V=\{s,a,b\}、辺と重みを

w(s,a)=1,w(s,b)=2,w(b,a)=2w(s,a)=1,\qquad w(s,b)=2,\qquad w(b,a)=-2

とします。真の最短距離は δw(s,a)=2+(2)=0\delta_w(s,a)=2+(-2)=0δw(s,b)=2\delta_w(s,b)=2 です。

アルゴリズムを走らせます。(0,s)(0,s) を処理して dist[a]1\texttt{dist}[a]\leftarrow1dist[b]2\texttt{dist}[b]\leftarrow2。ヒープは (1,a),(2,b)(1,a),(2,b) です。次に (1,a)(1,a) を取り出して処理しますが、aa から出る辺はないので何も起こりません。次に (2,b)(2,b) を処理して 2+(2)=0<12+(-2)=0<1 から dist[a]0\texttt{dist}[a]\leftarrow0 となり (0,a)(0,a) が押し込まれます。最後に (0,a)(0,a) を取り出すと 0=dist[a]0=\texttt{dist}[a] なので aa2 回目の処理を受けます。

この実装では最終的な dist[a]=0\texttt{dist}[a]=0 は偶然正しくなりますが、aa が 2 回処理されており Theorem 5.3 の手順 2 が破れています。頂点数が増えると処理回数は指数的に増えうるうえ、「取り出したら確定」として打ち切る標準的な実装では、aa の答えが 11 になって誤ります。

原因ははっきりしています。Theorem 5.3 の手順 4 の最後で使った「δw(s,y)δw(s,u)\delta_w(s,y)\le\delta_w(s,u)」は、yy から uu への残りの重みが非負であることに依存していました。負の辺があると、遠回りしてから重みを取り戻す経路がありうるので、暫定値が最小の頂点を確定してよい理由が消えます。

Remark 5.7

負の重みを許すなら、すべての辺の緩和を V1|V|-1 回繰り返すベルマン・フォード法を使います。O(VE)O(|V||E|) 時間かかる代わりに負の辺を扱え、V|V| 回目にまだ緩和できる辺があれば「ss から到達可能な負閉路がある」(最短距離が -\infty になる)ことを検出します。全点対の最短経路には、ワーシャル・フロイド法(O(V3)O(|V|^3))やジョンソンの方法があります。

なお「最短」ではなく「最長」の道を求める問題は、一般のグラフでは NP 困難(Definition 6.1)[P≠NP 予想とは何か] です(ハミルトン路問題が特別な場合として埋め込めます)。同じグラフの上の問題でも、少し向きを変えるだけで手に負えなくなることについては P≠NP予想とは何か を参照してください。

Exercise 6.1

V={1,2,,n}V=\{1,2,\ldots,n\} を頂点集合とする有向グラフ G=(V,E)G=(V,E) の隣接行列を AA とする(Definition 2.3)。任意の整数 k1k\ge1 について、行列 AkA^k(i,j)(i,j) 成分が「ii から jj への長さちょうど kk の歩道の本数」に等しいことを示してください。

Solution

kk についての帰納法で示します。

k=1k=1 のとき。ii から jj への長さ 1 の歩道とは辺 (i,j)(i,j) そのものなので、その本数は (i,j)E(i,j)\in E なら 1、そうでなければ 0 です。これは定義により AijA_{ij} に一致します。

kk で成り立つと仮定して k+1k+1 を示します。行列の積の定義から

(Ak+1)ij=l=1n(Ak)ilAlj(A^{k+1})_{ij} = \sum_{l=1}^{n} (A^{k})_{il}\, A_{lj}

です。一方、ii から jj への長さ k+1k+1 の歩道 (v0,,vk+1)(v_0,\ldots,v_{k+1}) は、最後から 2 番目の頂点 l=vkl=v_k によって、「ii から ll への長さ kk の歩道」と「辺 (l,j)(l,j)」の組に一意に分解されます。逆に、ll を任意に固定して「ii から ll への長さ kk の歩道」と「辺 (l,j)(l,j)」を選べば、つなげて ii から jj への長さ k+1k+1 の歩道が 1 つ得られます。この対応は全単射なので、本数は ll ごとの積の和、すなわち

l=1n(i から l への長さ k の歩道の本数)×(辺 (l,j) の有無)\sum_{l=1}^{n} (\,i \text{ から } l \text{ への長さ } k \text{ の歩道の本数}\,)\times(\,\text{辺 } (l,j) \text{ の有無}\,)

に等しく、帰納法の仮定によりこれは l(Ak)ilAlj=(Ak+1)ij\sum_l (A^k)_{il}A_{lj} = (A^{k+1})_{ij} です。

Exercise 6.2標準

無向グラフ G=(V,E)G=(V,E)二部グラフであるとは、V=XYV=X\sqcup Y(互いに素な和)と分割して、すべての辺が XX の頂点と YY の頂点を結ぶようにできることをいう。

GG を連結な無向グラフ、sVs\in V を任意の頂点とし、d[v]=δ(s,v)d[v]=\delta(s,v) を BFS で求めた距離とする。このとき

G が二部グラフ    すべての辺 {u,v}E について d[u]d[v]G \text{ が二部グラフ} \iff \text{すべての辺 } \{u,v\}\in E \text{ について } d[u]\ne d[v]

が成り立つことを示し、二部グラフ判定が O(V+E)O(|V|+|E|) 時間でできることを説明してください。

Solution

GG は連結なので、すべての vv について d[v]<d[v]<\infty です(Theorem 3.2)。

(\Leftarrow) すべての辺で d[u]d[v]d[u]\ne d[v] とします。Lemma 3.1 の証明の冒頭で示した三角不等式を、辺 {u,v}\{u,v\} の両向きに使うと d[v]d[u]+1d[v]\le d[u]+1 かつ d[u]d[v]+1d[u]\le d[v]+1、すなわち d[u]d[v]1|d[u]-d[v]|\le1 です。仮定より d[u]d[v]d[u]\ne d[v] なので d[u]d[v]=1|d[u]-d[v]|=1 となり、d[u]d[u]d[v]d[v] の偶奇は異なります。そこで

X={vVd[v] は偶数},Y={vVd[v] は奇数}X=\{v\in V \mid d[v] \text{ は偶数}\},\qquad Y=\{v\in V \mid d[v] \text{ は奇数}\}

と置けば、XY=VX\sqcup Y=V であり、すべての辺は XXYY をまたぎます。よって GG は二部グラフです。

(\Rightarrow) GG が二部グラフで分割 V=XYV=X\sqcup Y を持つとします。sXs\in X としてよいです。まず「ss から vv への任意の歩道の長さの偶奇は、vvXX にあるか YY にあるかだけで決まる」ことに注意します。実際、歩道に沿って辺を 1 本渡るたびに XXYY が入れ替わるので、長さが偶数なら終点は XX、奇数なら終点は YY です。したがって vXv\in X なら d[v]d[v] は偶数、vYv\in Y なら d[v]d[v] は奇数です。

いま辺 {u,v}\{u,v\} を任意に取ると、二部性から一方は XX、他方は YY に属するので、d[u]d[u]d[v]d[v] の偶奇は異なり、特に d[u]d[v]d[u]\ne d[v] です。

判定の計算量。 BFS で dd を求めるのに Proposition 3.3 より O(V+E)O(|V|+|E|)、隣接リストを 1 周して各辺で d[u]d[v]d[u]\ne d[v] を確かめるのに O(V+E)O(|V|+|E|)、合計 O(V+E)O(|V|+|E|) です。連結でない場合は連結成分ごとに BFS を行えば同じ評価です。

Exercise 6.3標準

負の重みを持つ有向グラフに対して、「すべての辺の重みに十分大きい定数 c>0c>0 を足して非負にしてから、ダイクストラ法で最短路を求める」という方針を考える。この方針は正しいでしょうか。正しくないなら反例を挙げ、どこが破綻しているかを説明してください。

Solution

正しくありません。反例を挙げます。V={s,a,b,t}V=\{s,a,b,t\}、辺と重みを

w(s,t)=2,w(s,a)=1,w(a,b)=1,w(b,t)=1w(s,t)=2,\qquad w(s,a)=1,\qquad w(a,b)=-1,\qquad w(b,t)=1

とします。ss から tt への道は s,ts,t(重み 2)と s,a,b,ts,a,b,t(重み 1+(1)+1=11+(-1)+1=1)の 2 本で、最短路は後者です。

すべての辺に c=1c=1 を足すと、w(s,t)=3w'(s,t)=3w(s,a)=2w'(s,a)=2w(a,b)=0w'(a,b)=0w(b,t)=2w'(b,t)=2 となり、すべて非負です。この新しい重みでは、s,ts,t の重みが 3、s,a,b,ts,a,b,t の重みが 2+0+2=42+0+2=4 となり、最短路が s,ts,t に変わってしまいます。

破綻の理由は、定数シフトが道ごとに違う量だけ重みを増やすことにあります。辺を kk 本使う道は kckc だけ重くなるので、辺の本数が多い道が不当に不利になり、大小関係が保存されません。

正しい付け替えは、頂点ごとの関数(ポテンシャル)h:VRh:V\to\mathbb{R} を使って w(u,v)=w(u,v)+h(u)h(v)w'(u,v)=w(u,v)+h(u)-h(v) とするものです。このとき道 u=p0,,pk=vu=p_0,\ldots,p_k=v の重みは

i(w(pi,pi+1)+h(pi)h(pi+1))=w(P)+h(u)h(v)\sum_{i}\bigl(w(p_i,p_{i+1})+h(p_i)-h(p_{i+1})\bigr) = w(P) + h(u)-h(v)

となり、同じ端点を持つ道はすべて同じ量 h(u)h(v)h(u)-h(v) だけずれるので、大小関係が保たれます。すべての辺で w0w'\ge0 となる hh は、Remark 5.7 のベルマン・フォード法で(負閉路がなければ)求められます。これがジョンソンの方法です。

Exercise 6.4

ダイクストラ法を、優先度付きキューの代わりに「未処理の頂点のうち dist\texttt{dist} が最小のものを、長さ V|V| の配列の線形探索で選ぶ」方式で実装すると O(V2+E)O(|V|^2+|E|) 時間になることを確かめてください。そのうえで、Proposition 5.4 の二分ヒープ実装がこの配列実装より漸近的に速くなるのは、E|E|V|V| のどのような範囲にあるときか答えてください。

Solution

配列実装の評価。 外側のループは「未処理の頂点から最小の dist\texttt{dist} を持つものを選んで処理する」を繰り返すもので、各頂点は 1 回だけ処理されるのでループは高々 V|V| 回まわります。1 回あたり、V|V| 個の要素を線形に走査して最小値を探すので O(V)O(|V|) です。ここまでで O(V2)O(|V|^2) です。

緩和については、辺 (u,v)(u,v)uu が処理されるときの 1 回だけ調べられ、配列に対する更新は O(1)O(1) です。よって緩和の総費用は Proposition 2.6 (b) より O(V+E)O(|V|+|E|) です。初期化は O(V)O(|V|) です。合計して O(V2+E)O(|V|^2+|E|) となります。単純グラフでは E=O(V2)|E|=O(|V|^2) なので、これは O(V2)O(|V|^2) とも書けます。

比較。 二分ヒープ実装は O(V+ElogV)O(|V|+|E|\log|V|)、配列実装は O(V2)O(|V|^2) です。前者が漸近的に速いのは、o 記法(Definition 3.2)[Complexity and Big-O Notation] で書けば

ElogV=o(V2),すなわちE=o ⁣(V2logV)|E|\log|V| = o(|V|^2), \qquad \text{すなわち}\qquad |E| = o\!\left(\frac{|V|^2}{\log|V|}\right)

のときです。

道路網のように次数が定数で抑えられるグラフでは E=Θ(V)|E|=\Theta(|V|) なので、ヒープ実装が Θ(VlogV)\Theta(|V|\log|V|)、配列実装が Θ(V2)\Theta(|V|^2) で、V=105|V|=10^5 なら約 10610^6101010^{10} と 4 桁の差がつきます。逆に密なグラフ E=Θ(V2)|E|=\Theta(|V|^2) では、ヒープ実装が Θ(V2logV)\Theta(|V|^2\log|V|)、配列実装が Θ(V2)\Theta(|V|^2) となり配列実装が速くなります。

  • T. H. Cormen, C. E. Leiserson, R. L. Rivest, C. Stein, Introduction to Algorithms, 4th ed., MIT Press, 2022 — 第 VI 部(グラフアルゴリズム)。基本的なグラフ探索、単一始点最短経路、全点対最短経路を扱う。邦訳は『アルゴリズムイントロダクション』近代科学社。
  • E. W. Dijkstra, “A note on two problems in connexion with graphs”, Numerische Mathematik 1 (1959), 269–271. DOI: 10.1007/BF01386390 — ダイクストラ法の原論文。最小全域木の構成法と合わせて 2 ページ半で述べられている。
  • M. L. Fredman and R. E. Tarjan, “Fibonacci heaps and their uses in improved network optimization algorithms”, Journal of the ACM 34 (1987), 596–615. DOI: 10.1145/28869.28874 — フィボナッチヒープにより、ダイクストラ法を O(E+VlogV)O(|E|+|V|\log|V|) に改善した論文。
  • J. Kleinberg and É. Tardos, Algorithm Design, Addison-Wesley, 2005 — 第 3 章(グラフ)と第 4 章(貪欲法)。BFS と DFS の性質、ダイクストラ法を貪欲法の枠組みで扱っている。
  • R. Sedgewick and K. Wayne, Algorithms, 4th ed., Addison-Wesley, 2011 — 第 4 章(Graphs)。実装寄りの記述と、隣接リストによるデータ構造の作り方が詳しい。
  • 石畑清『アルゴリズムとデータ構造』岩波書店(岩波講座 ソフトウェア科学 3)、1989 — グラフの表現と探索の日本語の標準的な解説。

Appendix: 優先度付きキューの選び方

Section titled “Appendix: 優先度付きキューの選び方”

取り出しと値の引き下げのどちらが多いか。 ダイクストラ法が優先度付きキューに要求する操作は 2 つで、最小値の取り出しは高々 V|V| 回(各頂点 1 回)、値の引き下げ(decrease-key)は高々 E|E| 回(各辺 1 回)起こります。疎でないグラフでは後者が圧倒的に多いので、引き下げが速い構造ほど有利です。

代表的な 3 つの実装。 下の表の計算量は、上の回数と 1 回あたりの費用を掛けたものです。

実装最小値の取り出し値の引き下げダイクストラ法全体
未整列配列の線形探索O(V)O(\lvert V\rvert)O(1)O(1)O(V2+E)O(\lvert V\rvert^2 + \lvert E\rvert)
二分ヒープO(logV)O(\log \lvert V\rvert)O(logV)O(\log \lvert V\rvert)O((V+E)logV)O((\lvert V\rvert+\lvert E\rvert)\log \lvert V\rvert)
フィボナッチヒープO(logV)O(\log \lvert V\rvert)(償却)O(1)O(1)(償却)O(E+VlogV)O(\lvert E\rvert + \lvert V\rvert\log \lvert V\rvert)

フィボナッチヒープの O(E+VlogV)O(|E|+|V|\log|V|) は、比較に基づく実装としては漸近的に最良ですが、定数倍が大きく実装も複雑なので、実務では二分ヒープが使われることがほとんどです。各データ構造の内部については 基本的なデータ構造 を参照してください。

decrease-key を持たない言語での書き方。 Python の heapq のように値の引き下げがない場合、本文の実装のように「引き下げの代わりに新しい要素を押し込み、取り出したときに古ければ捨てる」遅延削除を使います。ヒープに入る要素数が V|V| ではなく E+1|E|+1 まで増えますが、log(E+1)=O(logV)\log(|E|+1)=O(\log|V|) なので漸近計算量は変わりません(Proposition 5.4)。実装が単純になるぶん、こちらが好まれることが多いと思います。

Report an error in this article ・Operated by: Mugen Giken LLCPricingTermsLegal notice

© 2026 夢現技研合同会社 ・Feeding the text to an LLM is welcome. Code samples are MIT licensed.