Rubyでエイトクイーン問題をバックトラック法で解く

スポンサーリンク
スポンサーリンク
ライフスタイル関連のコンテンツ
お金 | 仕事 | 勉強 | プライベート | 健康 |
プログラミング関連のコンテンツ
C言語/C++入門 | Ruby入門 | Python入門 | プログラミング全般

という本を読んでいたら、エイトクイーン問題というのが出てきて面白そうでした。
再帰関数の練習がてら、Rubyプログラムで解いてみることにチャレンジ。

スポンサーリンク

エイトクイーン問題のルール

エイト・クイーン – Wikipedia

チェスの盤上に、8個のクイーンを配置する。このとき、どの駒も他の駒に取られるような位置においてはいけない。
クイーンの動きは、上下左右斜めの8方向に、遮る物がない限り進める。将棋の飛車と角行を合わせた動きである。

面白そうな問題です。
びっくりしたのは、100年以上前の1874年には解答が出ていたそうです。

バックトラック法でエイトクイーン問題を解く

とりあえず試してみて、ダメだったら引き返してやり直す、というアルゴリズムは、バックトラック法と呼ばれるそうです。
そういえば、正規表現のマッチはバックトラック法で行われると、なんかの本で読んだことがある。
以下、バックトラック法で試行錯誤を行い、エイトクイーン問題を解く。

# エイトクイーン問題
# 8×8マスのボード上に、8つのクイーンを置く
# 8つのクイーンは、上下2方向、左右2方向、斜め4方向、合計8方向へ移動可能とする。
# このとき、8つのクイーンはお互いに、進行方向でぶつからないように配置する。⇒利き筋
 
class EightQueen
  attr_reader :row
  def initialize
    # ボード上の座標(0, 0)~(7, 7)まで、マス目を@board[x][y]で表す。
    # xが縦軸、yが横軸になるので注意。
    @board = [
      [false, false, false, false, false, false, false, false],
      [false, false, false, false, false, false, false, false],
      [false, false, false, false, false, false, false, false],
      [false, false, false, false, false, false, false, false],
      [false, false, false, false, false, false, false, false],
      [false, false, false, false, false, false, false, false],
      [false, false, false, false, false, false, false, false],
      [false, false, false, false, false, false, false, false],
    ]
=begin
      # 以下の書き方でも良い
      @board = Array.new(8, false)
      @board.each_index do |i|
        @board[i] = Array.new(8, false)
      end
=end
    
    # 行(row), 列(col)のサイズ
    @row = @board.length  # ⇒8
    @col = @board[0].length  # ⇒8
  end
 
  
  # x は行番号、count は制御用。@row回再帰したら、trueを返す。
  # x に0~(@row-1)を渡して使う
  def solve_8q(x, count = 0)
    if (@row - 1) < x || x < 0
      return
    end
    if count == @row
      return true
    end
    @board[x].each_index do |i|
      # クイーンが(x, i)に置けるかどうかチェック
      if check(x, i)
        @board[x][i] = true
        #puts "デバッグ用:(#{x}, #{i}) にクイーンを置きました。"
        # 再帰で次の行へ。xは@row(8)以上にならず、0に巻き戻す
        if solve_8q( ((x + 1) % @row), (count + 1) )
          return true
        else
          # 次の行が失敗なら(x, i)をfalseに戻す
          @board[x][i] = false
        end
      end
    end
    return false
  end
 
  # ボードを出力
  def show_board
    @row.times do |x|
      @col.times do |y|
        if @board[x][y]
          print "■"
        else
          print "□"
        end
      end
      print "\n"
    end
  end
 
  # (x,y)の利き筋に、クィーンがあるかどうかチェック
  # 利き筋がなく、(x,y)にクイーンが置けるなら、trueを返す
  def check(x, y)
    # 上方向をチェック
    p = x - 1
    while 0 <= p
      return false if @board[p][y]
      p -= 1
    end
    # 下方向をチェック
    p = x + 1
    while p < @col
      return false if @board[p][y]
      p += 1
    end
 
    # 左方向をチェック
    q = y - 1
    while 0 <= q
      return false if @board[x][q]
      q -= 1
    end
    # 右方向をチェック
    q = y + 1
    while q <= @row
      return false if @board[x][q]
      q += 1
    end
 
    # 左上方向をチェック。x,yとも1ずつ小さくなる。
    p = x - 1
    q = y - 1
    while 0 <= p && 0 <= q
      return false if @board[p][q]
      p -= 1
      q -= 1
    end
    # 左下方向をチェック。xは大きく、yは小さくなる
    p = x + 1
    q = y - 1
    while p < @col && 0 <= q
      return false if @board[p][q]
      p += 1
      q -= 1
    end
 
    # 右上方向をチェック。xは小さく、yは大きくなる
    p = x - 1
    q = y + 1
    while 0 <= p && q < @col
      return false if @board[p][q]
      p -= 1
      q += 1
    end
    # 右下方向をチェック。x,yとも1ずつ大きくなる
    p = x + 1
    q = y + 1
    while p < @col && q < @row
      return false if @board[p][q]
      p += 1
      q += 1
    end
    
    return true
  end
end
 
row = EightQueen.new.row
row.times do |x|
  puts "----- 解答 #{x + 1} -----"
  puts
  eq = EightQueen.new
  eq.solve_8q(x)
  eq.show_board
  puts
end

上記のスクリプトは、以下のページのエイトクイーンのバックトラック法での解決を参考にしました。

アルゴリズム for Ruby

開始の位置が(0,0)の場合は、解を発見できるけれど、それ以外の開始位置では正しい解が発見できない。

とのことだったので、起点(開始の位置)を、(0,0)以外の(0,0) ~ (7, 0)とした場合でも動作するように改良しました。
ちなみに、ボード上の(7, 0)の位置は、配列上では@board[7][0]となる。
以下に、実行結果を示します。
“■”がクイーン、”□”は空のマス。

実行結果

----- 解答 1 -----
 
■□□□□□□□
□□□□■□□□
□□□□□□□■
□□□□□■□□
□□■□□□□□
□□□□□□■□
□■□□□□□□
□□□■□□□□
 
----- 解答 2 -----
 
□□□□□□■□
■□□□□□□□
□□■□□□□□
□□□□□□□■
□□□□□■□□
□□□■□□□□
□■□□□□□□
□□□□■□□□
 
----- 解答 3 -----
 
□□□■□□□□
□□□□□□□■
■□□□□□□□
□□■□□□□□
□□□□□■□□
□■□□□□□□
□□□□□□■□
□□□□■□□□
 
----- 解答 4 -----
 
□□□□□■□□
□□□■□□□□
□□□□□□■□
■□□□□□□□
□□■□□□□□
□□□□■□□□
□■□□□□□□
□□□□□□□■
 
----- 解答 5 -----
 
□□□■□□□□
□■□□□□□□
□□□□□□□■
□□□□□■□□
■□□□□□□□
□□■□□□□□
□□□□■□□□
□□□□□□■□
 
----- 解答 6 -----
 
□□□■□□□□
□□□□□■□□
□□□□□□□■
□■□□□□□□
□□□□□□■□
■□□□□□□□
□□■□□□□□
□□□□■□□□
 
----- 解答 7 -----
 
□□□□□■□□
□□□■□□□□
□■□□□□□□
□□□□□□□■
□□□□■□□□
□□□□□□■□
■□□□□□□□
□□■□□□□□
 
----- 解答 8 -----
 
□□■□□□□□
□□□□■□□□
□■□□□□□□
□□□□□□□■
□□□□□■□□
□□□■□□□□
□□□□□□■□
■□□□□□□□

ボード上の座標(0, 0) ~ (7,0) を起点として探索を開始して、8通りの答えを導くことができました。
一番左の列に注目すると、一つずつクイーンの位置(■)が下に下がっている。これが探索の開始地点。
・・・が、Wikipediaによると本当は、12通りの回答があるそうで、もれなく12通りを探し出すためには、もう一歩アルゴリズムの工夫が必要みたいです。
現在のアルゴリズムですと、起点1つにつき、一通りの解答しか得られない。
ここをどう改良したら良いんだろう・・・?バックトラックじゃなくて、総当りの方法にしなきゃ駄目なのだろうか。

参考・関連ページ
エイト・クイーン – Wikipedia
アルゴリズム for Ruby
バックトラック法
定番アルゴリズムを徹底理解! – 今からでも遅くない!アルゴリズム入門:selfup

スポンサーリンク
 
スポンサーリンク