Rubyで最短経路を探索しよう!
人生を書き換える者すらいた。: 人材獲得作戦・4 試験問題ほか
次に同じ質問がきたときに
「1時間いらないっしょ、こんなの」
と是非ともほざくために
今から勉強します
ダイクストラ法による最短経路探索
図におけるS点からG点に到達するための最短経路を求めたい
各ノードを結ぶエッジを糸としてS点をゆっくりと持ち上げた場合
緊張する糸が変移しながら最終的にS−B−D−Gを結ぶ糸が緊張して
これが最短経路と分かる*1
計算機上でこの現象をシミュレートしたものを
ダイクストラ法というらしい
今各ノードとそこから伸びるエッジの情報(コストと接続先)を渡して
その最短経路および総コストを出力するプログラムを考えてみよう
data = { :s => [[5, :a], [4, :b], [2, :c]], :a => [[5, :s], [2, :b], [6, :g]], :b => [[4, :s], [2, :a], [3, :c], [2, :d]], :c => [[2, :s], [3, :b], [6, :d]], :d => [[2, :b], [6, :c], [4, :g]], :g => [[6, :a], [4, :d]] } g = Graph.new(data) g.print_route(:s, :g) # => s(0) -> b(4) -> d(6) -> g(10)
各ノードをエッジ情報を備えたNodeクラスのオブジェクトとし
エッジ情報をEdgeクラスのオブジェクトとしてそれぞれ表現し
Graphクラス内で生成するようにしよう
各ノードオブジェクトには経路探索の経過および結果を保持するため
cost, done(確定ノードフラグ), from(直前ノードid)を用意する
class Node attr_accessor :id, :edges, :cost, :done, :from def initialize(id, edges=[], cost=nil, done=false) @id, @edges, @cost, @done = id, edges, cost, done end end class Edge attr_reader :cost, :nid def initialize(cost, nid) @cost, @nid = cost, nid end end class Graph def initialize(data) @nodes = data.map do |id, edges| edges.map! { |edge| Edge.new(*edge) } Node.new(id, edges) end end end
スタートsidからゴールgidまでの最短経路を求めるrouteメソッドと
その印刷用のprint_routeを定義しよう
class Graph def route(sid, gid) dijkstra(sid) base = @nodes.find { |node| node.id == gid } @res = [base] while base = @nodes.find { |node| node.id == base.from } @res << base end @res end def print_route(sid, gid) route(sid, gid) puts @res.reverse.map { |node| "#{node.id}(#{node.cost})" }.join(" -> ") end end
任意点nidのコストを求めるcostメソッドも定義しよう
class Graph def cost(nid, sid) dijkstra(sid) @nodes.find { |node| node.id == nid }.cost end end
そして探索アルゴリズムの核心であるdijkstraメソッドを定義しよう
class Graph private def dijkstra(sid) @nodes.each do |node| node.cost = node.id == sid ? 0 : nil node.done = false node.from = nil end loop do done_node = nil @nodes.each do |node| next if node.done or node.cost.nil? done_node = node if done_node.nil? or node.cost < done_node.cost end break unless done_node done_node.done = true done_node.edges.each do |edge| to = @nodes.find{ |node| node.id == edge.nid } cost = done_node.cost + edge.cost from = done_node.id if to.cost.nil? || cost < to.cost to.cost = cost to.from = from end end end end end
dijkstraメソッドでは次の処理をする
(1)スタートノードにコスト0をセットする
(2)確定ノードdone_nodeに最少コストのノードをセットする
(3)確定ノードのエッジの中からその接続先への総コストが最小となるよう接続先のコストを更新する
(4)未確定ノードがなくなるまで(2)(3)を繰り返す
じゃあデータを入れてみよう!
data = { :s => [[5, :a], [4, :b], [2, :c]], :a => [[5, :s], [2, :b], [6, :g]], :b => [[4, :s], [2, :a], [3, :c], [2, :d]], :c => [[2, :s], [3, :b], [6, :d]], :d => [[2, :b], [6, :c], [4, :g]], :g => [[6, :a], [4, :d]] } g = Graph.new(data) g.print_route(:s, :g) puts g.cost(:d, :s) g.print_route(:a, :c) # >> s(0) -> b(4) -> d(6) -> g(10) # >> 6 # >> a(0) -> b(2) -> c(5)
いいみたいだ
試験問題
Graphクラスがあればあの試験問題が解けるはずだ
まずは方針を考えよう
(1)迷路データを二次元配列として読み込む
(2)迷路上の各マス(スペースの箇所)をその座標がidである*2ノードオブジェクトとしてノードデータを生成しGraphオブジェクトを生成する
(3)最短ルートを求め該当ノードを$マークに置き換えた迷路を出力する
まずは迷路を読み込もう
maze = DATA.readlines.map { |line| line.chomp.split(//) } __END__ ************************** *S* * * * * * * ************* * * * * ************ * * * * ************** *********** * * ** *********************** * * G * * * *********** * * * * ******* * * * * * **************************
次にその座標がidになるようにノードデータを作り
それをGraphクラスに渡そう
隣接するノードのデータedgesを作るのにちょっと工夫がいる
nodes = {} maze.each.with_index do |line, y| line.each.with_index do |data, x| next if data == '*' id = data.match(/\w/) ? $& : "#{y}_#{x}" edges = [[-1, 0], [1, 0], [0, -1], [0, 1]].inject([]) do |mem, (_y, _x)| _x += x; _y += y case maze[_y][_x] when /\w/ then mem << $& when /\s/ then mem << "#{_y}_#{_x}" else mem end end.map { |nid| [1, nid] } nodes[id] = edges end end g = Graph.new(nodes)
最後に最短経路を求めて
元の迷路データ上に$マークをマッピングしよう
route = g.route('S', 'G') maze.each_with_index do |line, y| line.each_with_index do |data, x| print route.find { |pos| pos.id == "#{y}_#{x}" } ? '$' : data end print "\n" end
************************** *S* *$$$$$ * *$* *$ * $************* * *$*$$$* $$************ * *$$$ * $$$$$ * **************$*********** * $$$$$$$$$$$$$ * **$*********************** * $$$$$*$$$$$$$$$$$$$$G * * * $$$ *********** * * * * ******* * * * * * **************************
なんかよさげだ