経路探索アルゴリズムA*をRubyで実装してみた

前回書いた経路探索アルゴリズムA* - gan2 の Ruby 勉強日記
たくさんブクマされててちょっとびっくりです(;゚Д゚)


実装はFlash(Action Script)でやろうと思っていたのですが、
その前にRubyで書いてみることにしました。
途中、アルゴリズムの理解が不十分だったせいもあり、
多少てこづりましたがとりえず完成しました!
ソースはあんまり整理してないけども、
あまり気にせずに貼り付けておきます(ノ∀`)

=begin
**** 経路探索アルゴリズムA*(エースター) a-star.rb

**** アルゴリズムの概要
スタートノードから、あるノード n を通って、
ゴールまで辿り着くときの最短路経路を考える。
このとき、最短経路のコスト f(n) を次の式で表す。
f(n) = g(n) + h(n)
ここで、g(n) はスタートノードから n までの最小コスト。
h(n) は n からゴールノードまでの最短コストである。

g(n) と h(n) が一意に決まるのであれば、
全体の最短経路は容易に求まる。
しかし実際には、g (n) と h (n) をあらかじめ与えることは出来ない。
そこで、f(n) を
f*(n) = g*(n) + h*(n)
という推定値に置き換えて探索を行う。 

**** 参考
A* - Wikipedia
http://ja.wikipedia.org/w/index.php?title=A%2A

=end

# マップ
class Map
  # マップデータの書き込まれたファイルを読み込む
  def initialize(filename)
    @map    = []
    @width  = nil
    @height = nil

    x = 0
    y = 0
    open(filename) do |file|
      y = 0

      file.each do |line|
        x = 0
        ary = []

        line.chomp.split("").each do |c|
          node = Node.new(c, x, y)
          ary.push(node)
          if (c == "S")
            $startnode = node
          elsif (c == "G")
            $gallnode = node
          end

          x += 1            
        end

        @map.push(ary)
        y += 1
      end
    end

    @width  = x
    @height = y

    # スタートノード S の最短経路コストの推定値 f*(S) を求める。
    # 生成のとき g*(S) = 0 だから、
    # f*(S) = g*(S) + h*(S) より、f*(S) = h*(S) となる。
    $startnode.f_star = $startnode.h_star
  end
  
  attr_reader :width, :height

  # マップの表示
  def view
    @map.each do |m|
      m.each do |n|
        print n.type
      end
      puts
    end
    puts
  end

  # (デバッグ用) マップ上にあるノードの f* を表示
  def view_f_star
    @map.each do |m|
      m.each do |n|
        print n.f_star, " "
      end
      puts
    end    
  end
  
  def [](idx)
    @map[idx]
  end
end

# ノード
class Node
  # type はノードの種類("S", "G", "o", "x" のどれか)
  # x, y はマップ上の座標
  def initialize(type, x, y)
    @type = type
    @x = x
    @y = y
    @f_star = " "
    @parent = nil
  end

  attr_accessor :type, :x, :y, :f_star #, :parent
  attr_reader :parent
  
  # スタートノードから n までの最小コストの推定値 f*(n)
  def g_star
    @f_star - h_star
  end
  
  # n からゴールノードまでの最小コストの推定値 h*(n)
  # h*(n) には通常、n からゴールノードまでの直線距離を与えることが多い。
  # しかし、今回は斜めの移動を考慮しないため、
  # 縦と横の移動だけを使ったコストを求めたい。
  # よって、直線距離ではなく、n からゴールノードまでの
  # 縦と横の距離それぞれを足したものを h*(n) とする。
  def h_star
    cost($gallnode)
  end
  
  # ゴールノードであるかどうかの判定
  def is_gall_node?
    @type == "G"
  end

  # スタートノードであるかどうかの判定
  def is_start_node?
    @type == "S"
  end
  
  # 隣接しているノードを含む配列を返す
  # ここで、隣接しているとは縦横いずれかに隣り合っていることを指す。
  # よって、斜めに隣り合っているノードは結果の配列に含まれない。
  def adjacent_nodes
    m_list = []
    m = nil
    
    # 上
    if (@y - 1 >= 0 && $map[@y-1][@x].type != "x")
      m_list.push($map[@y-1][@x])
    end

    # 下
    if (@y + 1 < $map.height && $map[@y+1][@x].type != "x")
      m_list.push($map[@y+1][@x])
    end

    # 左
    if (@x - 1 >= 0 && $map[@y][@x-1].type != "x")
      m_list.push($map[@y][@x-1])
    end

    # 右
    if (@x + 1 < $map.width && $map[@y][@x+1].type != "x")
      m_list.push($map[@y][@x+1])
    end

    m_list
  end

  # ノード n から m へ移動するときのコスト
  # これは n, m 間の縦横の距離を足したものである。
  def cost(m)
    dis_x = (@x - m.x).abs
    dis_y = (@y - m.y).abs
    dis_x + dis_y
    
    # 直線距離をコストとする場合
    # 0.5(1/2)乗は平方根を求めるのと同じ
    # dis = (dis_x ** 2 + dis_y ** 2) ** 0.5    
  end

  # list 中に含まれているかどうかを調べる
  def is_included_in?(list)
    #puts "self #{self}"
    list.each do |l|
      #puts "l #{l}"
      # どうやらここが問題みたいだなぁ。
      return true if l == self
    end
    
    false
  end
  
  def parent=(p)
    @parent = p
    # puts "#{self}(#{@x}, #{@y}) の親は #{p}(#{p.x}, #{p.y})"
  end
end

# Openリストに格納されているノードの内、
# 最小の f*(n) を持つノード n を取り出す
def get_smallest_cost_node
  smallest = $openlist[0]
  index = 0

  $openlist.each_with_index do |n, idx|
    if n.f_star < smallest.f_star
      smallest = n 
      index = idx
    end
  end

  # Openリストから n を除いておく
  $openlist.delete_at(index)
  smallest
end

#### 
s = "S" # スタート
g = "G" # ゴール
o = "o" # 通行可能
x = "x" # 通行不能
# スタート・ゴールノード
# 値が入るのはマップを生成したとき
$gallnode  = nil
$startnode = nil
# マップの生成
$map = Map.new("map.txt")
$map.view
$openlist  = [] # 計算中のノード
$closelist = [] # 計算済のノード

# スタートノードをOpenリストに格納
$openlist.push($startnode)
#puts $startnode.f_star

# デバッグ用変数
count = 0
# Openリストが空になるまで探索
while ($openlist.size > 0)
  # Openリスト中で最小の f*(n) を持つノード n
  # このときOpenリストから n は外される
  n = get_smallest_cost_node
  #puts n.f_star
  #pp $openlist
  #puts "----"
  
  # n がゴールノードであれば探索を終了する
  if (n.is_gall_node?)
    puts "ゴールノードを発見しました。"
    puts
    break
  else 
    # それ以外の場合はCloseリストに n を入れる
    $closelist.push(n)
  end
  
  ## n に隣接している全てのノード m に対して以下の操作を行う
  m_list = n.adjacent_nodes # n に隣接している全てのノードを持つ配列
  m_list.each do |m|
    # f'(m) = g*(n) + h*(m) + COST(n,m) を計算する。
    # ここで COST(n,m) はノード n から m へ移動するときのコストである。
    # また g*(n) は g*(n) = f*(n) - h*(n) で求めることができる。
    f_dash = n.g_star + m.h_star + n.cost(m)
    #puts "m.x: #{m.x}/tm.y: #{m.y}"

    if (m.is_included_in?($openlist)) # m がOpenリストにある場合
      # f'(m) < f*(m) なら
      if f_dash < m.f_star
        # f*(m) = f'(m) に置き換えて
        m.f_star = f_dash 
        # m の親を n に更新
        m.parent = n
      end
    elsif (m.is_included_in?($closelist)) # m がCloseリストにある場合
      if f_dash < m.f_star
        m.f_star = f_dash 
        m.parent = n

        # m をCloseリストから外し、Openリストに移動
        index = nil
        $closelist.each_with_index do |c, idx|
          index = idx if c == m
        end
        $closelist.delete_at(index)
        $openlist.push(m)
      end
    else # どちらのリストにもない場合
      m.f_star = f_dash
      m.parent = n
      # m をOpenリストに追加
      $openlist.push(m)
    end
  end

  # デバッグのために無限ループ回避
  if count < 100
#     puts "count: #{count}"
#     puts "openlist: #{$openlist}"
#     puts "closelist: #{$closelist}"
#     puts "----"
#     count += 1
  else
    $map.view_f_star
    exit
  end
end

# puts $gallnode.parent
# puts $startnode.parent

pathlist = []
node = $gallnode
pathlist.push(node)

while (node.parent)
  node = node.parent
  pathlist.push(node)
end

pathlist = pathlist.reverse
# スタートノードを取り出す
pathlist.shift
pathlist.each do |n|
  puts "x: #{n.x}\ty: #{n.y}"
  # 通った道筋を "o" から "." に変える
  n.type = "."
end
puts
# 上の処理だとゴールノードも "." になってしまうので
# ここで "G" に戻しておく
pathlist.last.type = "G"

$map.view


上記のプログラムはmap.txtを読み込んで最短経路を探索します。
例えば以下のようなmap.txtを用意します。
Sがスタート。Gがゴール。
oが通れるところで、xが通れないところです。
SからGまでの最短経路を探索することになります。

ooooxxxG
oooooooo
xxooxxxx
oooooooo
oxxxxxoo
oooooooo
xxxoxxxo
Sooooooo

コマンドラインから実行すると、こんな結果が出力されます。

$ ruby a-star.rb
ooooxxxG
oooooooo
xxooxxxx
oooooooo
oxxxxxoo
oooooooo
xxxoxxxo
Sooooooo

ゴールノードを発見しました。

x: 1	y: 7
x: 2	y: 7
x: 3	y: 7
x: 3	y: 6
x: 3	y: 5
x: 4	y: 5
x: 5	y: 5
x: 6	y: 5
x: 6	y: 4
x: 6	y: 3
x: 5	y: 3
x: 4	y: 3
x: 3	y: 3
x: 3	y: 2
x: 3	y: 1
x: 4	y: 1
x: 5	y: 1
x: 6	y: 1
x: 7	y: 1
x: 7	y: 0

ooooxxxG
ooo.....
xxo.xxxx
ooo....o
oxxxxx.o
ooo....o
xxx.xxxo
S...oooo


今度はFlashで実装してみようと思います。