ダイクストラ法による単一始点最短経路を求めるアルゴリズム
グラフにおける単一始点最短経路問題とは、始点を固定した時に、他のすべての頂点への最短経路を求める問題のことです。
ダイクストラ法は、単一始点最短経路問題を解く時に利用され、利点としては
- 計算量が \(O(|E| \log |V|)\) であり、ベルマンフォード法の計算量 \(O(|E|×|V|)\) よりも高速に動作する
というのが挙げられます。一方で、欠点として
- 負の辺があるときに使用できない
というのがありますが、最短経路を求める非常に有名なアルゴリズムの一つです。
アルゴリズム
ダイクストラ法:
- 始点 s を「既に最短距離が確定した頂点」、他の頂点を「まだ最短距離が確定していない頂点」とする
- 以下をすべての頂点の最短距離が確定するまで繰り返す
- 全ての「既に最短距離が確定した頂点 u 」から「まだ最短距離が確定していない頂点 v 」へ伸びる全ての辺 e=(u,v) について、「v と d[v] の候補」をまとめておく
- 候補の中から、d[v] が最小のものを選択し、v を「既に最短距離が確定した頂点」に加える
※ 頂点集合を V・辺の集合を E とした、グラフ G=(V,E) において、始点 s から 頂点 v への最短距離を d[v] としています。
※ ダイクストラ法は、行ける場所の中から「最も早く行ける場所」を順番に確定させていきます。
計算量
- ヒープを使わない場合:
- \(O(|V|^2)\)
- ヒープを使う時:
- \(O(|E| \log |V|)\)
候補の中から d[v] が最小のものを選択するには、単純にやると全ての候補を確認することになるので \(O(|V|)\) の計算量がかかってしまいます。これを頂点数だけ繰り返すので、この場合の計算量は \(O(|V|^2)\) です。
しかし、ヒープなどのデータ構造を上手く使うと候補の追加や最小値の取得が \(O(\log |V|)\) で計算できます。この時の、更新と取り出しの回数は辺の数だけ生じるので \(O(|E|)\) です。
\(|E|\) が小さければ、後者の方が高速に動きます。
C++での実装例
ヒープを利用した時の実装例です。
C++ では STL の priority_queue をヒープとして利用することができます。
以下の実装では、仮の「頂点と最短距離」を全て priority_queue に入れているので、\(O(|E| \log |E|)\) の計算量になりそうですが、最短距離でない場合は無視をすることで、\(O(|E| \log |V|)\) になっています。(いずれにせよ \(\log |E| \leq \log |V|^2 = 2 \log |V|\) から同じ計算量になります。)
#include <bits/stdc++.h> using namespace std; struct Edge { long long to; long long cost; }; using Graph = vector<vector<Edge>>; using P = pair<long, int>; const long long INF = 1LL << 60; /* dijkstra(G,s,dis) 入力:グラフ G, 開始点 s, 距離を格納する dis 計算量:O(|E|log|V|) 副作用:dis が書き換えられる */ void dijkstra(const Graph &G, int s, vector<long long> &dis) { int N = G.size(); dis.resize(N, INF); priority_queue<P, vector<P>, greater<P>> pq; // 「仮の最短距離, 頂点」が小さい順に並ぶ dis[s] = 0; pq.emplace(dis[s], s); while (!pq.empty()) { P p = pq.top(); pq.pop(); int v = p.second; if (dis[v] < p.first) { // 最短距離で無ければ無視 continue; } for (auto &e : G[v]) { if (dis[e.to] > dis[v] + e.cost) { // 最短距離候補なら priority_queue に追加 dis[e.to] = dis[v] + e.cost; pq.emplace(dis[e.to], e.to); } } } }
最短距離の経路復元
ダイクストラ法で最短距離を求めるついでに、「1つ前にどの頂点を通ったか」を記録しておけば、終点から逆にたどっていくことで、最短距離の経路を復元することができます。
先程の関数を少し拡張すると以下のようになります。prev という vector に「1つ前にどの頂点を通ったか」記録しています。
/* dijkstra(G,s,dis,prev) 入力:グラフ G, 開始点 s, 距離を格納する dis, 最短経路の前の点を記録するprev 計算量:O(|E|log|V|) 副作用:dis, prevが書き換えられる */ void dijkstra(const Graph &G, int s, vector<long long> &dis, vector<int> &prev) { int N = G.size(); dis.resize(N, INF); prev.resize(N, -1); // 初期化 priority_queue<P, vector<P>, greater<P>> pq; dis[s] = 0; pq.emplace(dis[s], s); while (!pq.empty()) { P p = pq.top(); pq.pop(); int v = p.second; if (dis[v] < p.first) { continue; } for (auto &e : G[v]) { if (dis[e.to] > dis[v] + e.cost) { dis[e.to] = dis[v] + e.cost; prev[e.to] = v; // 頂点 v を通って e.to にたどり着いた pq.emplace(dis[e.to], e.to); } } } }
記録した点を用いて、後ろから始点 r へとたどっていくことで経路を復元することができます。
/* get_path(prev, t) 入力:dijkstra で得た prev, ゴール t 出力: t への最短路のパス */ vector<int> get_path(const vector<int> &prev, int t) { vector<int> path; for (int cur = t; cur != -1; cur = prev[cur]) { path.push_back(cur); } reverse(path.begin(), path.end()); // 逆順なのでひっくり返す return path; }
アルゴリズムの考え方と正当性
基本となる式と考え方
頂点集合を V・辺の集合を E とした、グラフ G=(V,E) において、始点 s から 頂点 t への最短距離を d[t] と表すことにします。
基本はベルマンフォード法と同じく以下の式が成立することを利用します。
- d[v] = min{ d[u] + ( 辺e=(u,v) の距離 ) }
この式を利用して、最短距離を始点 s に近い頂点から順番に決定していくことができれば、すべての頂点に対して最短距離を求めることが可能です。
最短距離を1つずつ確定させていく
ベルマンフォード法での問題点は何かというと、「ある頂点までの最短距離が求まったかどうか最後まで分からない」というところにありました。
しかし、「負の辺が存在しない」という仮定を入れると、「最短距離が求まった頂点」を順番に決定していくことができます。
ダイクストラ法では、最短距離を以下のように確定させていきます。
- 「最短距離が確定した点集合」とつながる頂点について、最短距離 d[i] の候補をまとめておく
- 候補の中から、最もd[i] が小さい頂点を「最短経路が確定した点集合」に加える
具体例で確認
先程の手順を具体例で確認してみましょう。
「最短距離が求まった頂点」を水色・「まだ最短距離が確定していない点」のうち水色のものと繋がっているものを灰色で表すことにします。
灰色のノードが、次に更新されるノードの候補です。
- s→2:d[2] = 1
- s→3:d[3] = 3
- s→1:d[1] = 5
という候補があるので、この中から距離が最小のものを選びます。
これで、s と 2 が「最短距離が確定した頂点」になりました。次の候補としては、
- s→2→1:d[1] = 1+2 = 3
- (s→1:d[1] = 5)
- s→3:d[3]= 3
があり、この中から距離が最小のものを選択します。(d[1]に関してはより距離が小さい方を残します。)
あとの候補の残りは以下1つだけです。
- s→3:d[3]= 3
3が確定した頂点になったので、4を灰色に変更しました。
あとの候補の残りは以下の通りです。
- s→3→4:d[4] = 3+1 = 4
これで終了です。これ以上頂点が更新されることはありません。
以上で見たように、「最短距離が小さいもの」から順番に頂点を確定させていくことができます。これは、「負の辺が存在しない」という仮定があったからできることです。
練習問題
- APJ GRL_1_A Single Source Shortest Path
- JOI 2008 予選 6 – 船旅:何度も最短距離を求めます
- [AtCoder] ARC 064 E – Cosmic Rays:距離は整数とは限りません
- [AtCoder] ABC035 D – トレジャーハント:単一始点だけではなく、「単一終点」の最短経路も考えます
- [AtCoder] SoundHound Inc. Programming Contest 2018 -Masters Tournament-
- JOI 2014 予選 5 – タクシー:幅優先探索なども活用してグラフの最短経路に帰着させます
- JOI 2016 予選 5 – ゾンビ島:こちらも幅優先探索を使います。実装重めです
ディスカッション
コメント一覧
まだ、コメントがありません