Rubyで富豪的プログラミングで8クイーンに挑戦そして玉砕

8クイーンというパズル問題があるよ


簡単に説明すると8x8のチェス盤があって
ここに飛車角の動きをする8つのクイーンを
互いが取り合わない位置に配置する問題だよ


エイト・クイーン - Wikipedia


8クイーンの組合せは全部で92通りあるそうだよ
それでループとか再帰とか
バックトラックとか動的計画とかの
アルゴリズムの手法を駆使して
その解法をプログラミングするというのが
プログラマーたちの頭の体操になっているんだ
ちょっと検索すれば大量の模範解答が得られるよ


で 僕もRubyを使って
この問題を解いてみようと思うんだけど
どうもこの手のアルゴリズム
僕は嫌いというか苦手みたいで
解法を試行錯誤したり
他の人の書いた模範解答を解読する気が
どうしても起きないんだよ
まあ僕の能力不足としか言いようがないんだけど..


それでも暫くその理由を考えてみたら
なんとなく原因が分かったんだよ
それはこの手の問題では
問と実装コードとの間に大きな隔たりがあるってことなんだ
つまり問と実装コードとの間には
「実装者の工夫の大きな塊」が存在していて
それが問と実装コードとの隔たりを大きくしてる
そう僕は感じるんだ


で この「工夫の大きな塊」は
その実装者毎にまちまちで
結果実装コードはその実装者固有のものになっている
これは言い換えれば
8クイーン問題をうまく抽象化できていない
とも言えると思うんだけど


できれば僕は自分の働きをできるだけ小さくして
Rubyにもっともっと働いて欲しいと願ってるんだけど..


そんなわけで..


問と実装コードとの間の隔たりをできるだけ小さくした
抽象度の高い富豪的プログラミング
エイトクイーンに挑戦してみたよ:)
まあ最初から玉砕するのは分かってるんだけどね..


ここではチェス盤のサイズとクイーンの数を
8に限定しない実装を試みるよ
クラス名をQueenSolverとして初期化時に
盤サイズとクイーン数を指定できるようにしよう
そしてsolveメソッドで解答の組合せを出力するよ

# queen_solver.rb
class QueenSolver
  def initialize(nth=8, queens=8)
    @nth = nth
    @queens = queens
  end
  
  def solve

  end
end

QueenSolver.new.solve # => 


さてsolveの中身なんだけど
ここでは富豪的アプローチでいくから
すべての可能な配置の組み合わせを用意して
そこから条件を満たさないつまり
一対でも衝突のあるクイーンのある組を取り除くことで
有効な組み合わせを見つけ出すよ


だからsolveメソッドの中身は
こんな感じになるよ

class QueenSolver

  def solve
    all_combinations.reject do |pattern|
      pattern.combination(2).any? { |a, b| conflict?(a, b) }
    end
  end
end


all_combinationsですべての組合せを作って
そこからrejectで不適合なパターンの組を除くんだ
rejectのブロックではそのパターン中のクイーン相互の衝突を見て
その一つでも衝突があったらそのパターンを除外するよ
そのためにcombinationしてるよ


で あとはsolveの補助関数としての
all_combinationsとconflict?を定義すればいいよ
まずはall_combinationsから


ここでちょっと
盤サイズ3x3 クイーン数2の簡単な例を考えてみるよ
盤の座標は次の9つになるよね

 [[0,0],[0,1],[0,2],
  [1,0],[1,1],[1,2],
  [2,0],[2,1],[2,2]]

この盤における2つのクイーンの配置の組合せは
9C2となって36通りあるよ

9C2 = 9! / 2(9-2)! = 36


この考えに従ってall_combinationsをRubyで表現すると
次のようになるんだ

class QueenSolver

  def all_combinations
    board_coordinates.combination(@queens)
  end
end


で盤の座標board_coordinatesは
repeated_permutationを使って次のように書けるよ

class QueenSolver

  def all_combinations
    board_coordinates.combination(@queens)
  end

  def board_coordinates
    [*0..@nth-1].repeated_permutation(2).to_a
  end
end

qs = QueenSolver.new(3,2)
qs.board_coordinates # => [[0, 0], [0, 1], [0, 2], [1, 0], [1, 1], [1, 2], [2, 0], [2, 1], [2, 2]]
qs.all_combinations.to_a # => [[[0, 0], [0, 1]], [[0, 0], [0, 2]], [[0, 0], [1, 0]], [[0, 0], [1, 1]], [[0, 0], [1, 2]], [[0, 0], [2, 0]], [[0, 0], [2, 1]], [[0, 0], [2, 2]], [[0, 1], [0, 2]], [[0, 1], [1, 0]], [[0, 1], [1, 1]], [[0, 1], [1, 2]], [[0, 1], [2, 0]], [[0, 1], [2, 1]], [[0, 1], [2, 2]], [[0, 2], [1, 0]], [[0, 2], [1, 1]], [[0, 2], [1, 2]], [[0, 2], [2, 0]], [[0, 2], [2, 1]], [[0, 2], [2, 2]], [[1, 0], [1, 1]], [[1, 0], [1, 2]], [[1, 0], [2, 0]], [[1, 0], [2, 1]], [[1, 0], [2, 2]], [[1, 1], [1, 2]], [[1, 1], [2, 0]], [[1, 1], [2, 1]], [[1, 1], [2, 2]], [[1, 2], [2, 0]], [[1, 2], [2, 1]], [[1, 2], [2, 2]], [[2, 0], [2, 1]], [[2, 0], [2, 2]], [[2, 1], [2, 2]]]


次にconflict?を定義しよう
クイーンは飛車角の動きをするから
縦 横 斜めの何れかでの衝突をもって
適合の判定をするよ

class QueenSolver

  def conflict?(a, b)
    conflict_x?(a, b) || conflict_y?(a, b) || conflict_cross?(a, b)
  end
end


conflict_x?とconflict_y?は
クイーンのxまたはy座標を比較すればいいから簡単だね
conflict_cross?の判定にはちょっと工夫がいるけど
それはx-y座標の和と差を比較することで判定できるんだよ
だからこれらのコードは次のようになるよ

class QueenSolver

  def conflict_x?(a, b)
    a[0] == b[0]
  end

  def conflict_y?(a, b)
    a[1] == b[1]
  end

  def conflict_cross?(a, b)
    a.inject(:-) == b.inject(:-) || a.inject(:+) == b.inject(:+)
  end
end


さあこれでQueenSolverの完成だよ
コードを再掲するよ



さてちゃんと動作するかテストするよ
Wikipediaによれば2クイーンと3クイーンには解がなくて
4には2つ 5には10 6には4つの解があるそうだよ



まずは2x2から4x4のテスト

% rspec queen_solver_spec.rb -c -fd -l5
Run filtered including {:line_number=>5}

QueenSolver
  solve 2x2 to 4x4
    solve 2x2
    solve 3x2
    solve 3x3
    solve 4x4
Finished in 0.02028 seconds
4 examples, 0 failures

うまくいってるね!


次は5x5

% rspec queen_solver_spec.rb -c -fd -l31
Run filtered including {:line_number=>31}

QueenSolver  solve 5x5
Finished in 0.37836 seconds
1 example, 0 failures

これもパスした


次は6x6

% rspec queen_solver_spec.rb -c -fd -l35
Run filtered including {:line_number=>35}

QueenSolver  solve 6x6
Finished in 15.05 seconds
1 example, 0 failures

これもOKだよ
ただ実行時間が15秒と結構掛かってるね..


さあ
いよいよエイトクイーン8x8をテストするよ

% rspec queen_solver_spec.rb -c -fd -l39
Run filtered including {:line_number=>39}

QueenSolver

.....

やっぱり帰ってこない!


8x8におけるall_combinationsの数は
64C8だから..

64C8 = 64! / 8!(64-8)! = 4426165368

44億パターン!


あと3年くらい経ったらこれくらいの計算は
Rubyでもさっと解けるようになるのかな..