アルゴリズム・データ構造 プログラミング

[アルゴリズム(Ruby)]ダイクストラ法の解説

アルゴリズム解説_ダイクストラ法

ダイクストラ法(Dijkstra's Algorithm)

ダイクストラ法は、グラフ上のある点から、ある点までの最短経路を求めるアルゴリズムで、コストに負数が含まれない時に使用できます。

例えば、カーナビで現在地から目的地までの最短経路を検索するような動作を実装することができます。

ノード(地点)の繋がりをグラフで表す

地点と地点やモノとモノの繋がり(ネットワーク)を表した図をグラフといい、点をノード(節)、点同士を繋ぐ線をエッジ(辺)、距離をコスト(重み)と言います。

例えば下図でノード0ノード1コスト4で行ったり来たり出来ることを表しています。(無向グラフ)
また、エッジを矢印にすることで一方通行を表現することもできます。(有向グラフ)

経路図

グラフを表すデータ構造

プログラミングでグラフを表すデータ構造として、隣接行列隣接リストがあります。
上図グラフの繋がりは以下のデータ構造で表現することが出来ます。

隣接行列
# 隣接行列
adjacency_matrix = [
  [nil,   4,   7, nil, nil,   9],
  [  4, nil,   2,   6, nil, nil],
  [  7,   2, nil,   3, nil,   5],
  [nil,   6,   3, nil,   5, nil],
  [nil, nil, nil,   5, nil,   7],
  [  9, nil,   5, nil,   7, nil]
]

隣接行列

隣接リスト
# 隣接リスト
adjacency_list = [
  [[1, 4], [2, 7], [5, 9]],
  [[0, 4], [2, 2], [3, 6]],
  [[0, 7], [1, 2], [3, 3], [5, 5]],
  [[1, 6], [2, 3], [4, 5]],
  [[3, 5], [5, 7]],
  [[0, 9], [4, 7]]
]

隣接リスト

アルゴリズム解説

ダイクストラ法のアルゴリズムについて解説します。
今回はグラフの繋がりを表すデータ構造に隣接リストを使います。

処理の流れ
  1. グラフの隣接リストadjacency_listを定義する
  2. 各ノードまでの最小コストmin_costs取り得ない値で初期化する
    (今回は nil とします)
  3.  探索リストsearch_list[初期コスト, 開始ノード番号]を追加する (※注)
  4. search_listに要素がある間実行するループを設定する
    1. search_listから最小コストの要素[cost, node]を取り出す
    2. 取り出したノードの最小コストmin_costs[node]が確定済みならループをスキップする
    3. 取り出したノードの最小コストmin_costs[node]を確定する
    4. 全ノードの最小コストが確定したらループを終了する
    5. 隣接ノード[next_cost, next_node]を順番に参照する
      1. 隣接ノードの最小コストmin_costs[next_node]が確定済みならループをスキップする
      2. search_listに隣接ノードまでのコストとノード番号[cost+next_cost, next_node]を追加する
  5. min_costsを参照する(各ノードまでの最小コストが格納されている)

注意ポイント

探索リストsearch_list について

探索候補を格納するデータ構造に自作の PriorityQueueクラス(優先度付きキュー) を使います。
PriorityQueueクラスのソースコードや詳細につきましては下記の記事をご参照下さい。

[データ構造(Ruby)]スタック・キュー・木構造の解説

  • PriorityQueueクラス
    要素の追加・取り出しが出来る構造になっていて、常に最小の要素を取り出すことができます。
    要素が配列の場合、先頭の要素を比較します。

    • insert(element)
      キューに要素を追加します。
    • extract
      キューから最小の要素を取り出します。
    • size
      キューの要素数を返します。

サンプルコード(全体)
# ダイクストラ法(Dijkstra's Algorithm)

require "./priority_queue"

# ----------------------------------
# 1. グラフの隣接リストadjacency_listを定義
# ----------------------------------
ADJACENCY_LIST = [
  [[1, 4], [2, 7], [5, 9]],
  [[0, 4], [2, 2], [3, 6]],
  [[0, 7], [1, 2], [3, 3], [5, 5]],
  [[1, 6], [2, 3], [4, 5]],
  [[3, 5], [5, 7]],
  [[0, 9], [4, 7]],
]
# NODE: ノード数, START: 開始ノード番号
NODE = 6
START = 0

# ----------------------------------
# 2. 最小コスト min_costs を nil で初期化
# ----------------------------------
min_costs = Array.new(NODE)

# ----------------------------------
# 3. 探索リストserch_listに
# [初期コスト, 開始ノード番号]を追加
# ----------------------------------
search_list = PriorityQueue.new
search_list.insert([0, 0])

# ----------------------------------
# 4. search_listに要素がある間繰り返す
# ----------------------------------
while search_list.size > 0

  # ----------------------------------
  # 4.1. 最小コストの要素[cost, node]を取り出す
  # ----------------------------------
  cost, node = search_list.extract

  # ----------------------------------
  # 4.2. min_costs[node]が確定済みならスキップ
  # ----------------------------------
  next if not min_costs[node].nil?

  # ----------------------------------
  # 4.3. 取り出したノードの最小コストを確定
  # ----------------------------------
  min_costs[node] = cost

  # ----------------------------------
  # 4.4. 全ノードの最小コストが確定したら終了
  # ----------------------------------
  break if not min_costs.any?(nil)

  # ----------------------------------
  # 4.5. 隣接ノードを順番に参照する
  # ----------------------------------
  ADJACENCY_LIST[node].each do |next_node, next_cost|
    # ----------------------------------
    # 4.5.1. 隣接ノードの最小コストが確定済みならスキップ
    # ----------------------------------
    next if not min_costs[next_node].nil?

    # ----------------------------------
    # 4.5.2. search_listに隣接ノードまでの
    # コストとノード番号[cost + next_cost, next_node]を追加
    # ----------------------------------
    search_list.insert([cost + next_cost, next_node])
  end
end

# ----------------------------------
# 5. min_costsを参照する
# ----------------------------------
min_costs.each_with_index do |cost, idx|
  puts "#{START} から #{idx} までの最小コストは #{cost.to_s.rjust(2)} です"
end

# 実行結果
# > 0 から 0 までの最小コストは  0 です
# > 0 から 1 までの最小コストは  4 です
# > 0 から 2 までの最小コストは  6 です
# > 0 から 3 までの最小コストは  9 です
# > 0 から 4 までの最小コストは 14 です
# > 0 から 5 までの最小コストは  9 です

 


処理の流れ(全体)

dijkstra_flow_all


処理の流れ(パート別)

初期設定 (1/69 ~ 3/69)

1. グラフの隣接リストadjacency_listを定義する

2. 最小コストmin_costsnilで初期化する

dijkstra_01_02

 


3. 探索リストsearch_list[初期コスト, 開始ノード番号] を追加

初期コスト 0 として、ノード 0 から探索を開始する。

dijkstra_03


search_list に要素がある間繰り返すループ

ノード0の処理 (4/69 ~ 15/69)

4.1. 最小コストの要素[cost, node]を取り出す → [cost = 0, node= 0]

4.2. min_costs[0]: false → スキップしない

4.3. min_costs[0] = 0ノード0最小コストを 0 で確定

4.4. min_costsが全て確定したか?: false → 終了しない

4.5. ノード0 の隣接ノードを参照
 → ノード1: [0+4, 1], ノード2: [0+7, 2], ノード5: [0+9, 5]search_list に追加

dijkstra_flow_node0


ノード1の処理 (16/69 ~ 26/69)

4.1. 最小コストの要素[cost, node]を取り出す→ [cost = 4, node= 1]

4.2. min_costs[1]: false → スキップしない

4.3. min_costs[1] = 4 → ノード1最小コストを 4 で確定

4.4. min_costsが全て確定したか?: false → 終了しない

4.5. ノード1 の隣接ノードを参照
ノード0 は確定済みなのでスキップ
ノード2: [4+2, 2], ノード3: [4+6, 3] を search_list に追加

dijkstra_flow_node1


ノード2の処理 (27/69 ~ 38/69)

4.1. 最小コストの要素[cost, node]を取り出す → [cost = 6, node= 2]

4.2. min_costs[2]: false → スキップしない

4.3. min_costs[2] = 6 → ノード2最小コストを 6 で確定

4.4. min_costsが全て確定したか?: false → 終了しない

4.5. ノード2 の隣接ノードを参照
ノード0, ノード1 は確定済みなのでスキップ
ノード3: [6+3, 3], ノード5: [6+5, 5] を search_list に追加

dijkstra_flow_node2


ノード3の処理 (39/69 ~ 50/69)

4.1. 最小コストの要素[cost, node]を取り出す
[cost = 7, node= 2] は確定済みなのでスキップ
[cost = 9, node= 3]

4.2. min_costs[3]: false → スキップしない

4.3. min_costs[3] = 9 → ノード3最小コストを 9 で確定

4.4. min_costsが全て確定したか?: false → 終了しない

4.5. ノード3 の隣接ノードを参照
ノード1, ノード2 は確定済みなのでスキップ
ノード4: [9+5, 4] を search_list に追加

dijkstra_flow_node3


ノード5の処理 (51/69 ~ 61/69)

4.1. 最小コストの要素[cost, node]を取り出す
[cost = 9, node= 5]

4.2. min_costs[5]: false → スキップしない

4.3. min_costs[5] = 9 → ノード5 の最小コストを 9 で確定

4.4. min_costsが全て確定したか?: false → 終了しない

4.5. ノード5 の隣接ノードを参照
ノード0, ノード2 は確定済みなのでスキップ
ノード4: [9+7, 4] を search_list に追加

dijkstra_flow_node5


ノード4の処理 (61/69 ~ 68/69)

4.1. 最小コストの要素[cost, node]を取り出す
[cost = 10, node= 3], [cost = 11, node= 5] は確定済みなのでスキップ
[cost = 14, node= 4]

4.2. min_costs[4]: false → スキップしない

4.3. min_costs[4] = 14 → ノード4 の最小コストを 14 で確定

4.4. min_costsが全て確定したか?: trueループ終了

dijkstra_flow_node4

各ノードの最小コストを参照する (69/69)

5. min_costsを参照する(各ノードまでの最小コストが格納されている)

dijkstra_69

今回のまとめ

  • 点と点の繋がりをグラフと言い、グラフの点はノード、辺はエッジ、距離をコスト(重み)と言います。
  • 隣接行列や隣接リストでグラフを表現することが出来ます。
  • ダイクストラ法は、スタートノード(コスト0)から、一番近い(コストが小さい)ノードを次々に確定させて、探索範囲を増やしていきます。
  • 最小コストの要素を探すためのデータ構造に優先度付きキューを使用すると処理が高速になります。

ダイクストラ法は最初難しく感じますが、理解すると「あ、結構単純だったな」となります。
紙にグラフを書いて手書きでコストを計算してみるのもオススメです!

参考文献

Pythonで体験してわかるアルゴリズムとデータ構造(西澤 弘毅、森田 光)

【PR】Ruby学習でお世話になった本




【PR】アルゴリズム学習でお世話になった本


アルゴリズム関連の技術書は大抵C/C++で書かれていますが、ある程度プログラムが出来れば、処理の流れは理解することが出来ます。






通称: 螺旋本。C++で解説されています。AOJ(Aizu Online Judge)を運営している会津大学の渡辺准教授が書いた本です。データ構造や計算量の内容から丁寧に書いてありますのでアルゴリズム学習の最初の参考書としてオススメです。







通称: 蟻本。C++で解説されています。競技プログラミング中級者の定番書と言われていて、競技プログラミングで利用できる色々なアルゴリズムを学ぶことが出来ます。かなり高度な内容も含まれているので1冊分を完全に理解するのにはかなりの時間がかかりそうですが、手元に置いて何度も読み返しています。







通称: チーター本。C#, C++, JAVAでTopcoderの問題を解説しています。初心者~中級者向けに書かれているので解説が非常に丁寧です。







Python3でアルゴリズムを解説している本です。講義スタイルの本で、図やフローチャートを使ってアルゴリズムとデータ構造についてしっかりと説明されています。代わりにコードは少なめです。Pythonコードが読めなくても十分理解できると思います。


-アルゴリズム・データ構造, プログラミング
-,

© 2024 じゃいごテック