Rubyで数独(ナンプレ)を解く(本当の解決編)
4月 27th, 2008 admin Posted in Ruby, 今日のコード |
前回で攻略したかと思われたナンプレですが、またもや間違った仮定で作っていたため解けない問題があるということがわかりました。(そんな問題があるって知らなかったんです。)
いくら「コードは間違ってても恥ずかしがらずに晒せ」と言ってもこう何度も間違えてたら流石に怒られるんじゃないかと不安になってきましたが、よく考えたら怒ってくれる人もいないので気にせず進めます。
今回はアルゴリズムをバックトラック法という方法を使うように変更しました。 バックトラック法というのはとにかく候補を順番に試していって矛盾が発生したら一つ戻るという方法です。矛盾が発生したらそれ以降の(矛盾を含んだ)パターンの探索は行わないので総当りと比べて効率がよく、どんなパターンであっても解を求めることができるのが特長です。
コード
RUBY:
-
require 'pp'
-
before = Time.now
-
t = Time.now
-
class Numpla
-
def initialize(sheet)
-
@sheet = sheet
-
@size = Math.sqrt(sheet.size).to_i
-
@side_size = Math.sqrt(@size).to_i
-
@possible = Array.new
-
end
-
-
def solve
-
#初期の状態から入力候補をリストアップする。
-
(0...@sheet.length).each do |i|
-
if @sheet[i] == 0
-
@possible[i] = get_possibles(get_neighbors(i))
-
else
-
@possible[i] = []
-
end
-
end
-
-
#探索開始
-
search(0)
-
return @sheet
-
end
-
-
def search(index)
-
return true if index == 81
-
-
-
if @sheet[index] != 0
-
#0でない場合はチェックするだけ(初期から数字が割り当てられていたら)
-
if check(index, @sheet[index]) && search(index + 1)
-
return true
-
end
-
else
-
#入力候補の数だけ調査する
-
@possible[index].each do |x|
-
if check(index,x)
-
@sheet[index] = x
-
unless search(index + 1)
-
#間違った値だった場合は取り消す
-
@sheet[index] = 0
-
else
-
return true
-
end
-
end
-
end
-
end
-
return false
-
end
-
-
#そのマスに入力できる可能性があるかどうか調べる
-
def check(index,x)
-
return get_possibles(get_neighbors(index)).index(x)
-
end
-
-
#当該マスxに関連するマスのインデックスを配列で返す
-
def get_neighbors(index)
-
get_neighbors_h(index) | get_neighbors_v(index) | get_neighbors_b(index)
-
end
-
-
#水平方向のマスをリストアップ
-
def get_neighbors_h(index)
-
neighbors = Array.new
-
horizone = index / @size * @size
-
@size.times {|i|
-
neighbors.push(i + horizone)
-
}
-
neighbors.delete(index)
-
neighbors
-
end
-
-
#垂直方向のマスをリストアップ
-
def get_neighbors_v(index)
-
neighbors = Array.new
-
vertical = index % @size
-
@size.times {|i|
-
neighbors.push(@size * i + vertical);
-
}
-
neighbors.delete(index)
-
neighbors
-
end
-
-
#3x3のブロックのマスをリストアップ
-
def get_neighbors_b(x)
-
tf = x / @side_size * @side_size
-
offset = tf / @size % @side_size
-
fc = tf - @size * offset
-
-
neighbors = Array.new
-
(0..(@side_size-1)).each do |j|
-
(fc+@size*j..fc+@size*j+(@side_size-1)).each do |i| neighbors.push(i) end
-
end
-
neighbors.delete(x)
-
neighbors
-
end
-
-
#すでに確定しているマスの情報から、置ける可能性のある数字を割り出す
-
def get_possibles(neighbors)
-
possibles = Array.new
-
(1..@size).each{|i| possibles.push i }
-
neighbors.each do |i|
-
possibles.delete(@sheet[i])
-
end
-
-
possibles
-
end
-
end
-
-
sheet = Array.new
-
sheet.concat [6,0,0,0,0,0,1,0,8]
-
sheet.concat [0,7,0,0,0,2,3,0,0]
-
sheet.concat [0,0,5,8,0,0,9,0,0]
-
sheet.concat [0,4,0,9,0,0,0,0,0]
-
sheet.concat [0,5,0,0,4,0,0,6,0]
-
sheet.concat [0,0,1,0,5,0,0,0,4]
-
sheet.concat [0,0,0,6,3,0,0,0,0]
-
sheet.concat [0,2,6,0,7,9,0,3,0]
-
sheet.concat [0,0,3,0,0,0,0,0,0]
-
-
width = 9
-
height= 9
-
-
-
foo = Numpla.new(sheet)
-
result = foo.solve
-
-
count = 1
-
result.each do |x|
-
print x
-
print ","
-
print "\n" unless count % 9> 0
-
count += 1
-
end
-
-
#処理時間
-
p Time.now - before
まとめ
13行目付近で初期の配置から候補を絞って、それを探索に使っているのだけど、この問題の場合でいうとこれをすると約6秒、やらないと約12秒とかなり計算時間に差が出た。今後はこの調子で計算時間を縮めたい。ちなみにこの問題での再帰の回数は14764回でした、がんばってるねー!
4月 27th, 2008 at 2:15:19
[...] 4月 26th, 2008 admin Posted in 今日のコード, Ruby | ※このコードでは解けない問題があることがわかったのでコードを書き直しました。 新しいコードはこちら「Rubyで数独(ナンプレ)を解く(本当の解決編)」 [...]
4月 27th, 2008 at 3:07:47
>よく考えたら怒ってくれる人もいないので気にせず進めます。
僕が怒ろうか(^-^;)
いやいや、コード晒すのは称賛に値するよ。
最近、他でもたまに見かけるけどよくやるなぁと。
でも晒す程、鍛えられるんだろうな。
僕も何かやった方がいいかなぁ。
4月 27th, 2008 at 15:33:30
>僕が怒ろうか(^-^;)
ほめられると伸びないタイプなので怒ってOK
読まれるコードだと思って書くと品質が2~3割ぐらいはアップしている感じがありますね。
これは「あのころは若かった」と振り返る準備なんだと思って続けます。
ヒロキのコードは是非読みたいよ!
11月 11th, 2008 at 2:19:41
[...] ある解を求めるときに、考えられるパターンをすべて試しながら解く方法のひとつです。迷路の右手法をイメージするといいんじゃないでしょうか。パワープレイすぎてスマートさはありませんが、結局この方法が早かったり、この方法でないと解けなかったりします。以前ナンプレに挑戦したときも結局このバックトラック法を使いました。 [...]