やわらかテック

興味のあること。業務を通して得られた発見。個人的に試してみたことをアウトプットしています🍵

Elixirでダイクストラ法を実装して最短経路を求めるまで

グラフと最短経路

こんなグラフのネットワークがあったとする
f:id:takamizawa46:20190503162633p:plain

Start, A, B, Goalをそれぞれノードといい
6,2,3...のような数字をエッジの重み(ここでは距離)という

StartからGoalへの最短経路は見て分かるように

Start -> B -> A -> Goal

になるかと思う
f:id:takamizawa46:20190503163004p:plain

これがこのグラフでのStartからGoalへの最短経路となる

ではこの最短経路をアルゴリズム使って求められへんの?っていう賢い人が現れる
それで開発されたのがダイクストラというアルゴリズム

しかし説明をすると長くなる & 図が大量に必要なので省略する
(すでに書籍や先駆者の有志の方が最高に分かりやすい説明をしてくださっているため)

ダイクストラ法 解説」 などで検索してみてください

データ構造について

まずはデータの構造を定める
ここ数日、アルゴリズムの実装をして感じていることは
アルゴリズムのロジックを組む際の難易度はデータ構造に大きく依存するということ

シンプルなデータ構造で表現できた場合にはコードはあまり複雑にならない
データ構造に無駄があったり、欠陥があるとその分、コード部分で苦労をする

「このデータ構造が最もふさわしい」というベストアンサーは決めにくいが
シンプルであればシンプルであるほど良いというのが自分の中での答えかなと思ってます

ダイクストラ法に必要な3つのデータ

  • グラフ情報(graph)
  • コスト情報(costs)
  • 親ノード情報(parents)

をそれぞれ以下のように定めた(書籍を参考に実装した)

グラフ情報

#それぞれのノードがどのノードに接続しているか(距離も付与)
#start: %{a: 6, b: 2},
#start --6--> a
#start --2--> b ということを表現している
graph = %{
  start: %{a: 6, b: 2},
  a: %{fin: 2},
  b: %{a: 3, fin: 5},
  fin: %{}
}

コスト情報

#startから接続しているノードまでの距離
#startと接続していないノードにはinfinityをセット
costs = %{
  a: graph.start.a,
  b: graph.start.b,
  fin: :infinity
}

親ノード情報

#startから接続しているノードのノード名
#そのほかはnilとしておく
parents = %{
  a: :start,
  b: :start,
  fin: nil
}

1つにまとめられそうな気もするが、あまり複雑化させるよりはいくつかに分別して方が経験上良い
データの準備は以上で次にコードを実装する

Elixirでのダイクストラ法の実装

まずは全体のコードをお見せします

defmodule Algo do
  def dijkstra(graph, costs, parents, processed) do
    node = _search_minimize_cost_in_graph(costs, processed)
    case node do
      :empty -> {costs, parents}
      _ ->
        neighbors = graph[node]
        {n_costs, n_parents} = _node_update(node, Map.keys(neighbors), costs[node], costs, neighbors, parents)
        dijkstra(graph, n_costs, n_parents, processed ++ [node])
    end
  end

  defp _node_update(_, [], _, costs, _, parents), do: {costs, parents}
  defp _node_update(node, [head | tail], cost, costs, neighbors, parents) do
    new_cost = cost + neighbors[head]
    if costs[head] > new_cost do
      n_costs = Map.put(costs, head, new_cost)
      n_parents = Map.put(parents, head, node)
      _node_update(node, tail, cost, n_costs, neighbors, n_parents)
    else
      _node_update(node, tail, cost, costs, neighbors, parents)
    end
  end
  
  def creater(res, start_atom, end_atom) do
    _creater(res, start_atom, end_atom, [end_atom])
  end
  def _creater(res, start_atom, next_atom, accum) do
    end_val = res[next_atom]
    if end_val == start_atom do
      [start_atom] ++ accum
    else
      _creater(res, start_atom, end_val, [end_val] ++ accum)
    end
  end

  defp _search_minimize_cost_in_graph(costs, processed) do
    rest_node = Map.keys(costs) |> Enum.filter(fn x -> x not in processed end)
    case rest_node == [] do
      true -> :empty
      false -> Enum.reduce(rest_node, :infinity, fn x, accum ->
        if accum > costs[x] do
          x
        else
          accum
        end
      end)
    end
  end
end

この先の説明部分については完全に自己満足なので
結果を先にお見せします

graph = %{
  start: %{a: 6, b: 2},
  a: %{fin: 1},
  b: %{a: 3, fin: 5},
  fin: %{}
}

costs = %{
  a: graph.start.a,
  b: graph.start.b,
  fin: :infinity,
}

parents = %{
  a: :start,
  b: :start,
  fin: nil
}

res = Algo.dijkstra(graph, costs, parents, [])
#res: {%{a: 5, b: 2, fin: 6}, %{a: :b, b: :start, fin: :a}}

{_a, b} = res
root = Algo.creater(b, :start, :fin)
#root: [:start, :b, :a, :fin]

別のグラフで試してみる

graph = %{
  start: %{a: 6, b: 2, c: 1},
  a: %{fin: 1},
  b: %{a: 3, fin: 5},
  c: %{a: 6, fin: 2},
  fin: %{}
}

costs = %{
  a: graph.start.a,
  b: graph.start.b,
  c: graph.start.c,
  fin: :infinity,
}

parents = %{
  a: :start,
  b: :start,
  c: :start,
  fin: nil
}

res = Algo.dijkstra(graph, costs, parents, [])
{_a, b} = res
root = Algo.creater(b, :start, :fin)

#root: [:start, :c, :fin]

良い感じですね 実装についてはここまでです
どんな動きをしているかが気になる方は以下をどうぞ

作成したモジュールについて

最短経路を求めるこのモジュールは4つの関数から成り立っている

  • dijkstra(メイン関数)
  • _node_update(条件に従いノード情報を更新する関数)
  • creater(dijkstraの結果から最短経路を作成する関数)
  • _search_minimize_cost_in_graph(最も手数が少ないノードを探索する関数)

順にコードを見ていく

dijkstra

def dijkstra(graph, costs, parents, processed) do
    #隣接しているノードの中で最も距離が近い(コストが低い)ノードを探索
    node = _search_minimize_cost_in_graph(costs, processed)
    case node do
      #もう探索できるノードがない場合
      :empty -> {costs, parents}
      _ ->
        #探索した最もコストの低いノードに隣接しているノードを取得(マッチ)
        neighbors = graph[node]
        #ノード情報をアップデート
        {n_costs, n_parents} = _node_update(node, Map.keys(neighbors), costs[node], costs, neighbors, parents)
        #今調べたノードを探索済み(accumに追加)して再帰
        dijkstra(graph, n_costs, n_parents, processed ++ [node])
    end
end

_node_update

#探索できるノードが無くなったら終了
defp _node_update(_, [], _, costs, _, parents), do: {costs, parents}
defp _node_update(node, [head | tail], cost, costs, neighbors, parents) do
  #次のノードまでの合計距離
  new_cost = cost + neighbors[head]
  #現時点で発見されているコストよりも別ルートの方が最短かどうか
  if costs[head] > new_cost do
    #コスト情報と親情報を上書き
    n_costs = Map.put(costs, head, new_cost)
    n_parents = Map.put(parents, head, node)
    _node_update(node, tail, cost, n_costs, neighbors, n_parents)
  else
    _node_update(node, tail, cost, costs, neighbors, parents)
  end
end

_node_updateが呼ばれる以前にこんなことが起きている

  • Startから最もコストが低いノードはBノード
  • Bノードに隣接しているノードを全て取得(Aとfin)

この情報を元に_node_updateが呼ばれる
例えばBノードからAノードへの探索をする場合には元々のコストは以下のようになっている

ノード名 距離(コスト)
A 6
B 2
fin infinity

しかし、_node_updateが呼ばれ現状のAルートへの道のりよりも
Bを経由してAに行った方が近いことが分かる

head = :a #Aノード
new_cost = cost + neighbors[head]
#new_cost = 2 + 3 -> 5
#このコストは現状の6よりも小さい

従ってコストを更新する

ノード名 距離(コスト)
A 5
B 2
fin infinity

さらに親ノードの情報も更新する
更新前

ノード名 距離(コスト)
A start
B start
fin infinity

更新後

ノード名 距離(コスト)
A B
B start
fin nil

creater
この関数についてはオマケなのであまり深く語る部分はない
dijkstraの戻り値の第2要素を使ってresからrootを生成しているだけ

res = %{a: :b, b: :start, fin: :a}
root = Algo.creater(res)

iex> root
[:start, :b, :a, :fin] #道順になっている

_search_minimize_cost_in_graph

defp _search_minimize_cost_in_graph(costs, processed) do
  #探索済みのノードを破棄
  rest_node = Map.keys(costs) |> Enum.filter(fn x -> x not in processed end)
  case rest_node == [] do
    true -> :empty
    #[:a, :b, :fin]から最小コストを持つノードを探索
    false -> Enum.reduce(rest_node, :infinity, fn x, accum ->
      if accum > costs[x] do
        x
      else
        accum
      end
    end)
  end
end

長くなってしまいましたが、おそらくコード自体はもっと短くできるかと思います
とりあえずは実装してみるを原動力に書いているのでその点はご了承ください。リファクタリングElixir力が高まってからチャレンジしてみようと思います