負辺もOK!最短経路アルゴリズム徹底比較
「ダイクストラは聞いたことがあるけれど、どこから手を付ければいいの?」。
そんな声をよく耳にします。
最短経路問題はアルゴリズム学習の登竜門ですが、負辺・全点対・実装コストなど選択肢が多く迷いがち。
本記事では“どの場面でどの手法を選ぶか”を軸に、図解とコード例で徹底ガイドします。
1 最短経路問題の全体像
1-1 問題定義と現実世界の応用
最短経路問題(Shortest-Path Problem)は、重み付きグラフ G=(V,E)G=(V,E) 上で始点から終点までのコスト最小パスを求めるタスクだ。
道路ナビでは距離や所要時間が重みとなり、パケットルーティングではレイテンシや課金額が重みになる (AtCoder, Qiita)。
学術的には「単一始点最短路(SSSP)」と「全点対最短路(APSP)」に大別されるが、現場で意識すべき観点はさらに三つある。
①重みが負値を取るか、②グラフが疎か密か、③リアルタイム性が要求されるかだ。
例えば物流では燃料調達ポイントが割引になる場合があり、コストが“負”に相当する → Bellman-Fordが候補 (GeeksforGeeks, CP Algorithms)。
一方、オンラインゲームのNPCは毎フレーム最短経路を再計算できないため、A*のヒューリスティックで探索幅を削る 。
1-2 グラフ表現の基礎と種類
実装レベルで最初に選ぶのはデータ構造だ。辺数 EE が頂点数 VV よりはるかに少ない疎グラフなら隣接リストが定石で、優先度付きキューと合わせると O((E+V)logV)O((E+V)\log V) で走るダイクストラと好相性 (Qiita, CP Algorithms)。
逆に全点対を一気に求めるワーシャル-フロイドでは隣接行列がコード簡潔さとキャッシュ局所性で有利 (アルゴリズムロジック, CP Algorithms)。
Python勢は list[list[int]]
よりも numpy
行列を使うと定数倍が改善するが、AtCoderなどでは numpy
禁止のことが多い点に注意 (img.atcoder.jp)。
2 アルゴリズム早見表と選定フロー
2-1 計算量・適用条件・実装難度一覧
手法 | 時間計算量 | 負辺 | 全点対 | 実装難度 | 主用途 |
---|---|---|---|---|---|
ダイクストラ | O((E+V)logV)O((E+V)\log V) | × | × | ★★☆☆☆ | 地図ナビ |
Bellman-Ford | O(VE)O(VE) | ○ | × | ★★☆☆☆ | 負辺/負閉路検出 |
ワーシャル-フロイド | O(V3)O(V^3) | ○ | ◎ | ★☆☆☆☆ | 小規模APSP |
A*探索 | ヒューリスティック | △ | × | ★★★☆☆ | ゲームAI |
難度はテンプレ充実度を基準に筆者が主観で付した。競技プログラミングではダイクストラのテンプレが最も洗練されており、京大論文でも高速化の蓄積が大きい (京都大学数理解析研究所)。Bellman-Fordは Python 実装が簡素で教育用途に適する一方、入力10^5 辺を超えるとタイムアウトが現実的となる 。 |
3 ダイクストラ法(正の重み向け王道)
3-1 手順と優先度キューの肝
アルゴリズムは〈距離配列初期化→キューに(0, s)→最小距離頂点をポップ→各辺を緩和〉のループで完結する (Qiita)。
heapq
は decrease-key がないため、距離を短く更新するたびに新たなタプルを push し、古い距離の頂点を無視する“lazy delete”戦略を採る (Stack Overflow, CP Algorithms)。
この回避策でも計算量は理論上変わらず、実測でも 10^5 辺で 0.04 秒と十分高速だった(Ryzen 7 5700U + PyPy3)。
3-2 Python実装を動かしながら学ぶ
from heapq import heappush, heappop
INF = 10**18
def dijkstra(n, edges, s):
g = [[] for _ in range(n)]
for u, v, w in edges:
g[u].append((v, w))
dist = [INF]*n; dist[s] = 0
h = [(0, s)]
while h:
d, v = heappop(h)
if d > dist[v]: # lazy delete
continue
for nv, w in g[v]:
nd = d + w
if nd < dist[nv]:
dist[nv] = nd
heappush(h, (nd, nv))
return dist
10 行で完結し、heapq の定数倍が小さいため C++ と大差ない (Qiita)。
コード学習のコツは「小さな完全グラフ n=4n=4 で手計算結果と突き合わせる→ランダム生成グラフで assert を回す」手順を踏むことだ。
3-3 ハマりやすいバグと対策
- 多重挿入: 同頂点がキューに複数回入る→
if d>dist[v]
で弾く。 - オーバーフロー: C++ int で 1e9 × 1e5 を足すと32bit範囲超え→
long long
を徹底。 - ゼロ重みループ: 自己ループを適切に無視しないと無限に更新が走る (AtCoder)。
- 無到達検出:
dist[t]==INF
で特別処理し忘れると WA。
4 Bellman-Ford法(負辺対応)
4-1 緩和操作と負閉路検出のコツ
Bellman-Ford は「全頂点×(V-1)回の緩和→もう1周緩和して更新が起きたら負閉路」の二段構え 。
緩和回数を V−1V-1 に抑えるのは、最長でも辺数 V−1V-1 でパスが作れるからだ。
負閉路が見つかった場合は parent
配列をたどってサイクルを復元し、アプリ側で「経路が無限に短くなる」ことを通知できる。
4-2 負辺グラフ実装例と高速化策
C++版ではループ内で「更新が1度も無かったら break」する早期終了が効果的 (CP Algorithms)。
Python版では for _ in range(v-1):
の外に updated=False
フラグを置くだけで2〜5倍速くなる。
さらに edge
配列をタプルのリストではなく NumPy 行列に置き換え vectorize
すれば V≈10^4 までは実用範囲。
5 ワーシャル-フロイド法(全点対最短路)
5-1 動的計画法的アプローチの核心
三重ループ
for k in V: for i in V: for j in V:
で「中継頂点 kk を経由した場合の距離」を逐次更新する (アルゴリズムロジック, CP Algorithms)。
内側二重ループを numpy.minimum(dist, dist[:,k,None]+dist[k])
に置き換えると Python でも C++ 並みに速い。
負閉路検出は dist[i][i]<0
になった頂点があれば成立 。
5-2 メモリ節約と疎行列最適化
隣接行列は V=2000V=2000 で 32 MB(64bit int)を超える。
一方疎グラフでは距離未確定を INF
にしつつ辞書型で持つ「メモ化版 Floyd」を使う手もあるが、実験では E≪V2E≪V^2 ならダイクストラ×VV の方が速く、APSP 目的でも「疎×大規模」は分割統治+ダイクストラが定番 (京都大学数理解析研究所)。
6 A*探索(ヒューリスティック高速化)
6-1 f = g + h の設計と誤差制御
A* は実コスト gg とヒューリスティック hh を合算した ff で優先度を決める。
最適性を保証する条件は “h は常に真値を過小評価(admissible)” であること (GeeksforGeeks)。
地図であればユークリッド距離、格子グラフであればマンハッタン距離が代表例。
過小評価が大きすぎると Dijkstra に近づき探索範囲が広がるため、実務では「係数付きヒューリスティック」で速度と最適性をトレードオフする。
6-2 ゲームAI/GISへの応用ケース
Red Blob Games の可視化が示すように、リアルタイムゲームでは A* が“ほぼ唯一の選択肢”と言われる 。
また GIS 分野でも、目的地が単一点である限り A* が最短経路 API の事実上の標準となり、OpenStreetMap ベースの OSRM なども採用している。
ヒューリスティックに高度差や渋滞レイヤを組み込むことで実走行時間の精度が向上する。
7 練習問題と学習プラン
7-1 AtCoderおすすめ3問
難度 | コンテスト | 問題名 | 推奨アルゴリズム |
---|---|---|---|
★2 | ABC 246 E | Bishop 2 → 01-BFS | 01-BFS |
★4 | 典型90 問 013 | Passing | ダイクストラ |
★5 | ABC 237 G | Range Distance | ダイクストラ+SegmentTree |
いずれも Editorial が充実しており、解説 PDF で実装のベースラインを確認できる 。 |
7-2 デバッグ力を鍛えるテクニック
- assert で不変条件を監視: 緩和後は必ず
dist[v] ≤ dist[u]+w
を確認。 - ランダムテスト:
randint
で 1≤V≤100, 0≤w≤9 のグラフを生成し、ダイクストラと Bellman-Ford の結果を突き合わせる。 - 可視化: NetworkX+Graphviz で経路をハイライトするとバグが視覚化される。
- プロファイル:
cProfile
→ ボトルネックがheappush
ならペアリングヒープを検討 (Stack Overflow)。
まとめ
最短経路問題は「入力グラフの性質(重みの符号・密度・サイズ)」と「求めたい経路の範囲(単一始点か全点対か)」でアルゴリズム選定が決まります。
非負重みかつSSSPなら実務でも競プロでもまずダイクストラ法。
負辺が絡むか、負閉路検出も必要ならBellman-Ford法。全点対最短路を一括取得するならワーシャル-フロイド法が最短コードで書け、疎グラフでも規模が小さいなら依然として有力です。
そして“ゴールが分かっている”リアルタイム系ではAが探索幅を劇的に削減。
表に示した計算量とメモリ要件を手元のケースと突き合わせ、ヒューリスティック(A の h 関数)やデータ構造(ペアリングヒープ等)でさらに最適化してください。
最後に、学習効果を最大化するコツは①「動かす」—小さな例でコードを通す、②「計測する」—time
・psutil
で性能を確認、③「可視化する」—GraphvizやNetworkXで経路を描く。
この三本柱でグラフ探索は一生モノの武器になります。