ダイクストラ法
特徴
ダイクストラ法は「負のコストがない」前提とすることで、アルゴリズムがシンプルになり、たんに最小コストのノードを優先的に選択するだけでよくなる。
もし負の値を持つコストが存在した場合は、そのノードにつながるノードのコストは普通に更新されるが、その新しいコストがその先のノードにも正確に伝播していくとは限らないので不整合が起きる。(その場合はベルマン-フォード法を使う)
最小コストを持つノードを毎回取得する部分は優先度付きキューに置き換えると超速くなる。
アルゴリズム
初期配置
どこも無限時間かかる。
渋谷から出発
出発点なので渋谷は0分で行ける。
渋谷→大崎
60分もかかる。
渋谷→巣鴨
1分で行ける。
ここで渋谷のターンが終わる。
巣鴨のターン
ベルマン-フォード法ではここで渋谷を除くどの街を選んでもよかったのに巣鴨を選んだ理由は4つの街の中でいちばんコストが小さかったから。最初に渋谷から始めた理由も0分で行ける渋谷のコストがいちばん小さかったから。で、巣鴨からは秋葉原までは計2分でいけることがわかる。
巣鴨から行けるのは秋葉原だけだったのでここで巣鴨のターンを終了する。
秋葉原のターン
ここで秋葉原を選んだ理由は巣鴨のときのように渋谷と巣鴨を除いたなかでいちばんコストが小さかったから。そして末広町までは計3分で行けることがわかった。
もしゴールを目的地を末広町と考えているのならここで終わる。大崎なんか60分以上が確定しているのでこれ以上計算しても無駄だからである。
末広町のターン
一応続けると「大崎 vs 末広町」は末広町の勝ちなので末広町のターンになったものの行き先がなかったので何もしない。
大崎のターン
ここまで60分もかかるせいで最後になってしまった大崎は、マイナスのコストが使えないというルールがあるせいで、これ以上の挽回は難しい。とりあえず秋葉原まで2時間で行けることがわかったが、もちろん2分で行けることが確定している秋葉原を更新することはできなかった。
すべての街を一周したのでこれで終わる。
ここまでの更新履歴
最短 | 渋谷 | 巣鴨 | 大崎 | 秋葉原 | 末広町 | |
---|---|---|---|---|---|---|
1 | 渋谷(0) | 1 | 60 | |||
2 | 巣鴨(1) | 2 | ||||
3 | 秋葉原(2) | 3 | ||||
4 | 末広町(3) | |||||
5 | 大崎(60) |
街を回る順番がコスト順になっているのがわかる。
実装例
graph = {
"渋谷" => { "巣鴨" => 1, "大崎" => 60, },
"巣鴨" => { "秋葉原" => 1, },
"秋葉原" => { "末広町" => 1, },
"大崎" => { "秋葉原" => 60, },
"末広町" => { },
}
start = "渋谷"
predecessor = {}
distance = Hash.new(Float::INFINITY)
distance[start] = 0
heap = Heap.new { |e| distance[e] }
heap << start
until heap.empty?
from = heap.pop
graph[from].each do |to, cost|
v = distance[from] + cost
if v < distance[to]
predecessor[to] = from
distance[to] = v
heap << to
end
end
end
distance # => {"渋谷"=>0, "巣鴨"=>1, "大崎"=>60, "秋葉原"=>2, "末広町"=>3}
predecessor # => {"巣鴨"=>"渋谷", "大崎"=>"渋谷", "秋葉原"=>"巣鴨", "末広町"=>"秋葉原"}
route_to = -> v { v && [*route_to[predecessor[v]], v] }
route_to["末広町"] # => ["渋谷", "巣鴨", "秋葉原", "末広町"]
小手先の TLE 対策
AtCoder では実行時間が 2 秒を超えると TLE 判定になる。ダイクストラ法を使って解く問題はその TLE になりやすい。
そこで「街」を「配列インデックス」として使えるように 0 ベースの符号なし整数となるように調整する。
具体的には次のようにグラフを配列で持つようにする。
graph = [
[[1, 1], [3, 60]],
[[2, 1]],
[[4, 1]],
[[2, 60]],
]
そうすれば 0.2 秒ぐらい稼げる。
また距離のハッシュも配列にする。
distance = Array.new(graph.size, Float::INFINITY)
これで 0.1 秒ぐらい稼げる。
これらの小細工により 2 秒超えてしまう処理が 1.8 秒ぐらいに収まったりする。
その反面、
start = 0
の 0 が何の街なのかもわからない世界になってしまうので、あくまで競プロ攻略のための手段として気にとめておく。
内部データ管理方法
重要なのは、最小コストで移動できる街をどれだけ速く連続で取り出せるかである。それに合わせて変更容易性なども考慮するといろいろな書き方ができる。
訪問したら除去
unvisited = graph.keys # => ["渋谷", "巣鴨", "秋葉原", "大崎", "末広町"]
distance[start] = 0
from = unvisited.min_by { |e| distance[e] } # => "渋谷"
unvisited.delete(from) # => "渋谷"
distance[to] = new_cost
- 良い点
- 開始地点を設定するときに distance を更新するだけでよい
- 悪い点
- min_by の部分がボトルネックで競プロ問題で TLE になる
訪問したら追加
visited = []
distance[start] = 0
visited << start
from = visited.min_by { |e| distance[e] } # => "渋谷"
visited.delete(from) # => "渋谷"
distance[to] = new_cost
visited << to
- 良い点
- 訪問したときに visited も更新しないといけないが、取り出すのが速くなる
- 悪い点
- min_by の部分がボトルネックで競プロ問題で TLE になる
min_by を使わない
visited = []
distance[start] = 0
visited << [0, start]
cost, from = visited.min # => [0, "渋谷"]
visited.delete([cost, from]) # => [0, "渋谷"]
distance[to] = new_cost
visited << [new_cost, to]
- 良い点
- 強いて言えば min_by 相当がない言語でも動く
- 悪い点
- min の部分がボトルネックで競プロ問題で TLE になる
二分探索
heap = []
distance[start] = 0
heap << start
from = heap.shift # => "渋谷"
distance[to] = new_cost
i = heap.bsearch_index { |e| distance[e] > new_cost } || heap.size # => 0
heap.insert(i, to) # => ["巣鴨"]
- 良い点
- 実用的な速度で動作する
- 悪い点
- 強いて言えば bsearch_index の書き方が難しい
- 参考
優先度付きキュー
heap = Heap.new
distance[start] = 0
heap << [0, start]
_, from = heap.pop # => [0, "渋谷"]
distance[to] = new_cost
heap << [new_cost, to]
- 良い点
- 取り出す処理が O(1) になる
- 悪い点
- 優先度付きキューに、コストを含めるのが冗長
- 優先度付きキューを用意する必要がある
親切な優先度付きキュー
min_by のように比較条件を添えることができる優先度付きキューを使う場合。
heap = Heap.new { |e| distance[e] }
distance[start] = 0
heap << start
from = heap.pop # => "渋谷"
distance[to] = new_cost # 必ず先
heap << to # 必ず後
- 良い点
- 取り出す処理が O(1) になる
- min_by を置き換えただけのようなシンプルなコードになる
- 悪い点
- 必ず「distance を更新してから heap に追加しなければならない」暗黙の決まりがコードからは見えにくい
- 親切な優先度付きキューを用意する必要がある
過去問
問題 | 解法 | 考察 |
---|---|---|
典型90 013 - Passing | ゴールからも進める | LINK |
ABC340 D - Super Takahashi Bros. | ダイクストラ法に気づく | LINK |
Discussion