2015年1月3日土曜日

SATソルバのお試し(後編)

前回「SATソルバのお試し(前編)」の続きとして、SATソルバで何か例題を解いてみる。

検索すると、数独を例題としているサイトがあったので、それに倣ってみる。

実は、このサイトではruby-minisatを使って解いているので、ソースコードそのまんまでも動く。
でも、それでは学習にならないので、解説だけは参考にしながら独自に実装。
後述するが、このコードは汚いので、追試する人は先ほど紹介した参考サイトを見たほうが良いだろう。
#
# * 2 | * 4
# 1 * | * *
# ---+---
# * * | 3 *
# * * | * *
#

require "minisat"
solver = MiniSat::Solver.new

arr =
  (0...4).map do
    (0...4).map do
      (0...4).map { solver.new_var }
    end
  end

arr.each do |row|
  row.each do |square|
    solver << square
  end
end

for k in 0...4 do
  for j in 0...4 do
    solver << [-arr[0][j][k], -arr[1][j][k]]
    solver << [-arr[0][j][k], -arr[2][j][k]]
    solver << [-arr[0][j][k], -arr[3][j][k]]
    solver << [-arr[1][j][k], -arr[2][j][k]]
    solver << [-arr[1][j][k], -arr[3][j][k]]
    solver << [-arr[2][j][k], -arr[3][j][k]]
  end
end

for k in 0...4 do
  for i in 0...4 do
    solver << [-arr[i][0][k], -arr[i][1][k]]
    solver << [-arr[i][0][k], -arr[i][2][k]]
    solver << [-arr[i][0][k], -arr[i][3][k]]
    solver << [-arr[i][1][k], -arr[i][2][k]]
    solver << [-arr[i][1][k], -arr[i][3][k]]
    solver << [-arr[i][2][k], -arr[i][3][k]]
  end
end

solver << arr[0][1][1]
solver << arr[0][3][3]
solver << arr[1][0][0]
solver << arr[2][2][2]

solver.solve

ans =
  arr.map do |row|
    row.map do |square|
      (1..4).find do |i|
        solver[square[i - 1]]
      end
    end
  end

ans.each do |row|
  puts row.join(" ")
end
実行した結果は、以下の通り。
$ ruby sudoku.rb
3 2 1 4
1 4 2 3
4 1 3 2
2 3 4 1
$
どうやら正しい結果が出てるっぽい。
しかし、参考サイトとソースコードを比べると、僕のはとても汚い。

まず、同じ行に同じ数字が登場しない、というルールを表すのに、
for k in 0...4 do
  for j in 0...4 do
    solver << [-arr[0][j][k], -arr[1][j][k]]
    solver << [-arr[0][j][k], -arr[2][j][k]]
    solver << [-arr[0][j][k], -arr[3][j][k]]
    solver << [-arr[1][j][k], -arr[2][j][k]]
    solver << [-arr[1][j][k], -arr[3][j][k]]
    solver << [-arr[2][j][k], -arr[3][j][k]]
  end
end
とやってしまっている所が良くない。
ループを回すのに直値を使ってしまっているし、1行の中の4マスから任意に2マスを選択するところは、全組み合わせを手書きしてしまっている。

この部分は、参考サイトでは、
# 各行に同じ数字が 2 回以上現れない
field.each do |row|
  row.transpose.each do |vars|
    vars.combination(2) do |x, y|
      solver << [-x, -y]
    end
  end
end
となっており美しい。

僕が知らなかった、transposeやcombinationといったメソッドを利用している。
transposeは、配列の行と列を転置するメソッドの様だ。
それから、combinationはその名の通り組み合わせを返してくれるらしい。
こういうメソッドを知っていると、配列のサイズが変更になったときにも対応できる美しいコードが書ける。
いや、知らなくても、こういうメソッド無いかな?って調べれば良かったんだ。
もし、調べてみても見つからなかったら、自分でメソッドを書いても良かったはずだ。
反省。
その他、zipやらflattenなどの、配列を上手く操作するメソッドを多様されていて、とても勉強になった。

さて、このように便利なSATソルバだが、半導体のフロアレイアウトの自動決定などに使われている、ということを最近知った。会員制の記事で恐縮だが、興味のある方は一読をおススメする。

それにしても、機械に自動的に問題を解かせる、ってのは男のロマンだな。久しぶりに、手を動かしながらワクワク感を覚えた。

SATソルバのお試し(前編)

以前から使ってみたかったSATソルバを使ってみたので、その記録。

SATソルバのひとつminisatというのが、非常に高速と評判なので、これを使ってみよう。
SATソルバへは、連言標準形の形式で命題論理式を与える必要がある。
業界標準?では、DIMACS CNFという記法が一般的らしいけど、命題論理が全て数値で記述されており、とても読みにくい。
式が大きくなると書くだけで大変になるのは明白である。
そこで、命題論理式を書く部分を多少なりとも楽にするために、今回はRubyからminisatを利用できるruby-minisatを使ってみる。

以下、Ubuntu-14.04上で実行した。
$ sudo gem install ruby-minisat
Building native extensions.  This could take a while...
ERROR:  Error installing ruby-minisat:
        ERROR: Failed to build gem native extension.

        /usr/bin/ruby1.9.1 extconf.rb
/usr/lib/ruby/1.9.1/rubygems/custom_require.rb:36:in `require': cannot load such file -- mkmf (LoadError)
        from /usr/lib/ruby/1.9.1/rubygems/custom_require.rb:36:in `require'
        from extconf.rb:1:in `<main>'

Gem files will remain installed in /var/lib/gems/1.9.1/gems/ruby-minisat-2.2.0 for inspection.
Results logged to /var/lib/gems/1.9.1/gems/ruby-minisat-2.2.0/ext/minisat/gem_make.out
$
いきなりエラー。
そこで、このページを参考にして、ruby-devとg++をインストール。
$ sudo apt-get install ruby-dev
(略)
$sudo apt-get install g++
(略)
$
その後、以下の通りして、ruby-minisatのインストール完了。
$ sudo gem install ruby-minisatBuilding native extensions.  This could take a while...
Successfully installed ruby-minisat-2.2.0
1 gem installed
Installing ri documentation for ruby-minisat-2.2.0...
Installing RDoc documentation for ruby-minisat-2.2.0...
$
まずは、ruby-minisatのWebサイトにある、例題を解かせてみる。
例題は、以下の通りの簡単なもの。
# solve (a or b) and (not a or b) and (a or not b)

require "minisat"
solver = MiniSat::Solver.new

a = solver.new_var
b = solver.new_var

solver << [a, b] << [-a, b] << [a, -b]
p solver.solve  #=> true (satisfiable)

p solver[a]  #=> true
p solver[b]  #=> true
冒頭のコメント行にもある通り、
(a or b)、(not a or b)、(a or not b)の全てを満たす、aとbの組み合わせは存在するか?
という問題。
ここでのaとかbは、TrueかFalseの2値を取る。
この問題では、aとbが両方ともTrueの時に、命題全体が成立する。

これをsample.rbとして保存して実行。
結果は、以下の通りとなった。
$ ruby sample.rb
true
true
true
命題論理を与えるときに、
solver << [a, b] << [-a, b] << [a, -b]
なんて書けちゃうところが、大変楽チン。
ruby-minisatを作って下さった方に感謝。

というわけで、とりあえず、最初の一歩を踏み出した。
後編では、もうちょっとパズルっぽい問題を解かせてみたいと思う。