Rubyでエイトクイーン問題をバックトラック法で解く
お金 | 仕事 | 勉強 | プライベート | 健康 | 心
プログラミング関連のコンテンツ
C言語/C++入門 | Ruby入門 | Python入門 | プログラミング全般
という本を読んでいたら、エイトクイーン問題というのが出てきて面白そうでした。
再帰関数の練習がてら、Rubyプログラムで解いてみることにチャレンジ。
エイトクイーン問題のルール
チェスの盤上に、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
上記のスクリプトは、以下のページのエイトクイーンのバックトラック法での解決を参考にしました。
開始の位置が(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