cv.cm

コーディング · アルゴリズム

ダイクストラ法とは?仕組み・コード・計算量をわかりやすく解説

約8分で読めます

結論

ダイクストラ法は、辺の重みがすべて非負のグラフで、1つの開始ノードから他のすべてのノードへの最短経路を求めるアルゴリズムです。まだ確定していないノードのうち、現時点の距離が最小のものを選んで確定させ、そこから出る辺を緩和する、という操作を繰り返します。二分ヒープの優先度付きキューを使うと、計算量は O((V + E) log V) です。辺の重みに負の値があると、誤った答えになります。

ダイクストラ法が解く問題

幅優先探索は辺の数が最小の経路を見つけますが、道路、ネットワークの回線、航空便には、それぞれ異なる距離、コスト、遅延があります。ダイクストラ法は、こうした重み付きグラフでの単一始点最短経路問題を解きます。開始ノードを与えると、他のすべてのノードへの最小の合計コストと、それを実現する経路が求まります。

エッガー・ダイクストラ(Edsger W. Dijkstra)は、1959年に論文 "A note on two problems in connexion with graphs" でこのアルゴリズムを発表しました。

考え方を言葉で説明すると

  1. 開始ノードの距離を 0、ほかのすべてのノードの距離を無限大にします。
  2. 未確定のノードのうち距離が最小のものを選びます。そのノードの距離はここで確定します。
  3. 重み w の各出辺 u → v を緩和します。dist[u] + w < dist[v] なら dist[v] = dist[u] + w に更新し、v の親として u を記録します。
  4. 到達可能なすべてのノードが確定するまで、手順2と3を繰り返します。

なぜ最小の距離は確定してよいのでしょうか。そのノードに至るほかの経路は、距離がすでに同じかそれ以上のノードを経由しなければならず、重みが非負なら残りの辺は費用を増やすだけだからです。

具体例で一歩ずつ確認

辺 A→B(4)、A→C(1)、C→B(2)、B→D(1)、C→D(5)、D→E(3)を持つ有向グラフです。A から始めます。

ステップ確定するノードABCDE
開始—0∞∞∞∞
1A (0)041∞∞
2C (1)03(C 経由)16(C 経由)∞
3B (3)0314(B 経由)∞
4D (4)03147
5E (7)03147

B は、直接の辺でまず距離 4 になり、その後 C 経由で 3 に改善されます。E への最短経路は A → C → B → D → E で、合計コストは 1 + 2 + 1 + 3 = 7 です。

heapqを使ったPythonでの実装

Pythonの heapq モジュールは二分最小ヒープを提供します。「キーの減少」操作の代わりに、距離が改善されるたびに新しい (distance, node) のペアをプッシュし、古くなったペアは取り出したときに読み飛ばします。これを遅延削除(lazy deletion)と呼びます。

import heapq
def dijkstra(graph, start):
    dist = {start: 0}
    parent = {start: None}
    heap = [(0, start)]
    while heap:
        d, u = heapq.heappop(heap)
        if d > dist[u]:
            continue                      # outdated entry
        for v, w in graph[u]:
            nd = d + w
            if nd < dist.get(v, float("inf")):
                dist[v] = nd
                parent[v] = u
                heapq.heappush(heap, (nd, v))
    return dist, parent
graph = {
    "A": [("B", 4), ("C", 1)], "B": [("D", 1)],
    "C": [("B", 2), ("D", 5)], "D": [("E", 3)], "E": [],
}
dist, parent = dijkstra(graph, "A")
print(dist)  # {'A': 0, 'B': 3, 'C': 1, 'D': 4, 'E': 7}

特定の目的地までの経路を得るには、親へのリンクを開始ノードまでたどります。

def path_to(parent, target):
    path = []
    while target is not None:
        path.append(target)
        target = parent[target]
    return path[::-1]
print(path_to(parent, "E"))  # ['A', 'C', 'B', 'D', 'E']

1つの目的地までの距離だけが必要なら、その目的地がヒープから取り出された時点で探索を打ち切れます。その時点で距離は確定しているからです。

時間計算量と空間計算量

優先度付きキュー時間備考
二分ヒープ(heapq)O((V + E) log V)通常はこれを選びます。遅延削除では、ヒープに最大 E 個の要素が入りますが、log E は高々 2 log V なので、1操作あたり O(log V) のままです。
単純な配列で最小値を走査O(V²)E が V² に近い非常に密なグラフに向いています。
フィボナッチヒープO(E + V log V)理論上は最良ですが、定数が大きいため実際に速いことはほとんどありません。

空間計算量は、グラフ、距離と親の対応表、ヒープを合わせて O(V + E) です。

バリエーションとよくある間違い

  • if d > dist[u]: continue のチェックを忘れても誤りにはなりませんが、古い要素まで処理してしまい、大幅に遅くなることがあります。
  • (distance, node) ではなくノードだけをプッシュすると、ヒープの順序が意味をなさなくなります。
  • ノードが比較できないオブジェクトの場合、距離が同じだと heapq がノード同士を比較して TypeError を起こします。一意のカウンタを使って (distance, counter, node) をプッシュしてください。
  • A*探索は、優先度にヒューリスティックを加えたダイクストラ法です。残りの距離のよい推定値があれば、探索するノード数を減らせます。
  • 双方向ダイクストラ法は、開始ノードとゴールの両方から探索を行い、2つの探索が出会ったところで終了します。

使う場面と実世界での用途

グラフアルゴリズム
重みなし、またはすべての重みが等しいBFS、O(V + E)
重みが非負で、始点が1つダイクストラ法
負の重みがあるが、負閉路はないベルマン–フォード法、O(V × E)
全点対、小さく密なグラフフロイド–ワーシャル法、O(V³)
目的地が1つで、距離の推定値があるA*探索
  • ルーティング:OSPFやIS-ISなどのリンクステート型ルーティングプロトコルは、ダイクストラ法に基づく最短経路優先(SPF)計算を行います。
  • 地図とナビゲーション:実運用のシステムでは、ダイクストラ型の探索に前処理を組み合わせています。
  • コーディング面接の問題:ネットワーク遅延時間、最安の航空券(経由回数の制限付き)、グリッドでの最小労力の経路、グラフ上の「合計コスト最小」を求めるあらゆる問題。

よくある質問

ダイクストラ法は負の重みでも動きますか?

いいえ。最小の暫定距離を持つノードは、あとからどの経路を通ってもそれ以上安くならない、という前提に立っており、負の辺はこの前提を壊します。代わりにベルマン–フォード法を使ってください。

ダイクストラ法の時間計算量はどれくらいですか?

二分ヒープなら O((V + E) log V) です。単純な配列なら O(V²) で、密なグラフではこちらのほうが有利な場合があります。

ダイクストラ法は貪欲法ですか?

はい。各ステップで局所的に最良の選択、つまり最も近い未確定ノードの確定を行います。すべての重みが非負であれば、この選択が正しいことが証明されています。

ダイクストラ法とBFSの違いは何ですか?

BFSは通常のキューを使って辺の数を数えるため、すべての辺の重みが等しい場合にしか正しくありません。ダイクストラ法は合計距離で順序づけた優先度付きキューを使うため、異なる非負の重みを扱えます。

ダイクストラ法は全ノード間の最短経路を求められますか?

1回の実行で求まるのは1つの始点からの最短経路です。全点対が必要なら、すべてのノードから実行するか、小さく密なグラフにはフロイド–ワーシャル法を使います。

コーディング講座の一覧