数独をプログラムで解いてみる


解けたら天才! フィンランドの数学者が作った「世界一難しい数独」

 頭の体操がてらに、数独を解くプログラムを書いてみました。

とりあえず、データ構造はこんな感じで持とうと思います。

cels = [
  0,0,5,3,0,0,0,0,0,
  8,0,0,0,0,0,0,2,0,
  0,7,0,0,1,0,5,0,0,
  4,0,0,0,0,5,3,0,0,
  0,1,0,0,7,0,0,0,6,
  0,0,3,2,0,0,0,8,0,
  0,6,0,5,0,0,0,0,9,
  0,0,4,0,0,0,0,3,0,
  0,0,0,0,0,9,7,0,0
]

同じ列・行・ブロック(太線で囲まれた3×3)に入るマスを取得する関数は必要になるかな、と特に何も考えずコーディング

def mklist start, count, step
  count.times.map{|i| start + i*step}
end
def last_one clist
  clist if clist.size == 1
end
def get_effect_area idx
  get_row_area(idx) +
    get_col_area(idx) +
    get_block_area(idx)
end
def get_row_area idx
  mklist((idx - idx % 9), 9, 1)
end
def get_col_area idx
  mklist(idx%9, 9, 9)
end
def get_block_num idx
  x = idx % 3
  y = idx / 9 % 3
  idx - (x + y*9)
end
def get_block_area idx
  fh = get_block_num idx
  mklist(fh, 3, 1) +
    mklist(fh+9, 3, 1) +
    mklist(fh+18, 3, 1)
end

さて、普段自分が数独を解くときのことを考えると、戦略としては

  • そのマスに入り得る数字が1つのマスを探す
  • 同じ行のマスのうち、そのマスにしかある数字が入らないようなマスを探す

という2つの観点で解いてる気がします。

そのマスに入り得る数字の候補を各マス毎に持つとすると、こんな感じでしょうか。

work = cels.map{|c| c == 0 ? (1..9).to_a : [c] }

初めは、すべてのマスに1〜9までの数字を持たせておいて、そのマスに入る可能性が消えた数字を配列から取り除きます。このデータ構造を使って上記の2つの考え方を試してみます。

def print_nums nums
  row_sepalater = ['+','+','+','+'].join('-'*7)
  nums.each.with_index do |n, i|
    puts '|' if i % 9 == 0 and i > 0
    puts row_sepalater if i % (9*3) == 0
    print '| ' if i % 3 == 0
    print n.to_s + ' '
  end
  puts '|', row_sepalater
end

def rest_count work
  work.map{|c|c.size}.reduce(:+)
end

def find_comp_cell area, cels, work
  (1..9).each do |i|
    cand = area.select{|r| work[r].include? i}
    cels[cand[0]] = i if cand.size == 1
    return false if cand.size == 0
  end
  return true
end

100.times do 
  cels.each.with_index do |c, idx|
    if c == 0
      cels[idx] = work[idx][0] if work[idx].size == 1
    else
      get_effect_area(idx).each do |ei|
        work[ei].delete(c) if ei != idx
      end
    end
  end

  key_cel.each do |idx|
    find_comp_cell(get_row_area(idx), cels, work)
    find_comp_cell(get_col_area(idx), cels, work)
    find_comp_cell(get_block_area(idx), cels, work)
  end

  break if rest_count(work) <= 9*9
end

print_nums cels

結果

+-------+-------+-------+
| 0 0 5 | 3 0 0 | 0 0 0 |
| 8 0 0 | 0 5 0 | 0 2 0 |
| 0 7 0 | 0 1 0 | 5 0 0 |
+-------+-------+-------+
| 4 0 0 | 0 0 5 | 3 0 0 |
| 0 1 0 | 0 7 3 | 0 0 6 |
| 0 0 3 | 2 0 0 | 0 8 0 |
+-------+-------+-------+
| 0 6 0 | 5 0 0 | 0 0 9 |
| 0 0 4 | 0 0 0 | 0 3 0 |
| 0 0 0 | 0 0 9 | 7 0 0 |
+-------+-------+-------+

駄目か...。そうすんなりとは行きませんでした。後は、残りのマスに、数字を仮置きしてみて、矛盾が起きたらトラックバックという方法がありますが、あまり格好よくないなあ。

先ほど作った処理を関数化し、再起関数呼び出しを使ってトラックバックを実現しました。

def check1 cels, work
  cels.each_with_index do |c, idx|
    if c == 0
      cels[idx] = work[idx][0] if work[idx].size == 1
    else
      get_effect_area(idx).each do |ei|
        work[ei].delete(c) if ei != idx
      end
    end
  end
end

def find_comp_cell area, cels, work
  (1..9).each do |i|
    cand = area.select{|r| work[r].include? i}
    cels[cand[0]] = i if cand.size == 1
    return false if cand.size == 0
  end
  return true
end

def check2 cels, work
  r = true
  key_cel = [0,12,24,28,40,52,56,68,80]
  key_cel.each do |idx|
    r &&= find_comp_cell(get_row_area(idx), cels, work)
    r &&= find_comp_cell(get_col_area(idx), cels, work)
    r &&= find_comp_cell(get_block_area(idx), cels, work)
    return false unless r
  end
  return true
end

def deep_copy obj
  Marshal.load Marshal.dump(obj)
end

def resolve cels, work
  prev = 9**3
  while prev > rest_count(work) do
    prev = rest_count(work)
    check1 cels, work
    return :conflict unless check2 cels, work
    return :resolved if rest_count(work) == 9*9
  end
  return :continue
end

# 仮に数字を置いてみて、矛盾したらトラックバック
# を繰り返し、すべてのパターンを試す
def try_fill cels, work
  # 数字を置ける候補が少ないほうが効率がいいはず
  idx, lst = work.each_with_index.map{|w,i|[i,w]}.
    select{|i,w|w.size > 1}.min_by{|i,w|w.size}

  lst.each do |n|
    cels_new = deep_copy cels
    work_new = deep_copy work
    cels_new[idx] = n
    work_new[idx] = [n]

    case resolve(cels_new, work_new)
    when :resolved
      print_matrix cels_new
      return :resolved
    when :continue 
      if try_fill(cels_new,work_new) == :resolved
        return :resolved 
      end
    end
  end
  :conflict
end

# ---- start ----
case resolve(cels, work)
when :resolved then print_matrix cels
when :continue then try_fill cels,work
when :conflict then raise  
end

解けた!

+-------+-------+-------+
| 1 4 5 | 3 2 7 | 6 9 8 |
| 8 3 9 | 6 5 4 | 1 2 7 |
| 6 7 2 | 9 1 8 | 5 4 3 |
+-------+-------+-------+
| 4 9 6 | 1 8 5 | 3 7 2 |
| 2 1 8 | 4 7 3 | 9 5 6 |
| 7 5 3 | 2 9 6 | 4 8 1 |
+-------+-------+-------+
| 3 6 7 | 5 4 2 | 8 1 9 |
| 9 8 4 | 7 6 1 | 2 3 5 |
| 5 2 1 | 8 3 9 | 7 6 4 |
+-------+-------+-------+

もっといい方法もありそうですが、自分ではこれが限界でした。折を見てまた考えてみようかと思います。

それにしても、コンピュータで解いてもあまり感動がありませんね(^^;