数独ジェネレータ完成?

数独の問題ジェネレータができた気がする。「気がする」というのは、第三者が作ったソルバーで実際に問題として成立しているか検証していないから。

ともあれ、以下のコードを加えて適当にマトリックスから数字を消してみた。

def delete_one
  srand
  candidate = (0..80).to_a
  candidate.delete_if{|i| @matrix[i] == 0}
  c = candidate.pick_one
  if(uniq_solution?(c))
    @matrix[c] = 0
  end
end

def uniq_solution?(n)
  i = @matrix[n]
  x = n % 9
  y = n / 9

  (1..9).to_a.delete_if{|n| n == i}.each{|j|
    if(!tate(x).include?(j) &&
       !yoko(y).include?(j) &&
       !block(which_block(x,y)).include?(j))
      return false
    end
  }
end

すでに空いているセルをのぞいて、何か数字の入っている場所をランダムにピックアップし、そこに別解があるかどうか(uniq_solution?)をチェックしている。

出てきたマトリックスを眺めていると、何となく数独として成り立っているような気もするけど、ともかくヒント数が多くて初心者問題ばかり。だいたい36〜50個ぐらいは数字が残ってしまう。一般的な数独の問題がどの程度のヒント数か知らないけど、たぶん上級問題とかだと20個前後なのではないか。数学的に検証している人たちによれば、どうも16個あたりが下限という。

-------------------
|  2 5|    1|     |
|3 9 1|    4|     |
|  8 4|  9  |2 5  |
-------------------
|  3  |5 2  |  1  |
|2 6 7|  1  |  8  |
|     |    7|4 9  |
-------------------
|1 5  |9    |  7 6|
|8    |7 6  |     |
|     |1 8 2|    3|
-------------------
ヒント数は36個。今回のプログラムでは割とよくできたほうの問題だけど解くのはやたら簡単

ランダムな間引きじゃなくて、例えばマトリックスの対称性を考えたほうがいいのかもしれない。あるいは、いったん間引きが行き詰まったら、再び何らかの方法で数字を戻したりしてかき回したほうがいいんだろうか。

ランダムなアプローチではこのへんが限界なのか、あるいは、そもそも何か考え違いをしているような気もする。うーん、考え違いの気がしてきた。もっと減らせそうか?

ともあれ、Rubyの習作としては結構楽しかったし、ほどよい課題が次々と現れていい練習になった。特にArrayの扱いでいろいろと試せた。

#!/usr/bin/ruby

=begin
= sudoku
* データ構造は
  matrix = [1,2,3,4,,,,,,,81]
  yoko_index = [[0,1,2,...8], [9,10,11...17],
  tate_index = [[0,9,18,...72], [1,10,19...73],
  block_index = [[0,1,2,9,10,11,],[3,4,5,12,],,]
* blockは
  |0|1|2|
  |3|4|5|
  |6|7|8|
  という構造で、matrix[block_index(3)
* ひょっとして class Cellでindex(x,y)あたりを抽象化するとうまくいく?
違うかなー
class Cell
  attr_accessor :x, :y, :index
  def initialize
  end
end
=end

class Matrix
  attr_accessor :yoko_index, :tate_index, :block_index, :matrix

  def initialize
    @matrix = Array.new(81,0)
#    @filled_cell = Array.new

    @yoko_index = Array.new
    (0..8).each{|i|
      @yoko_index[i] = Array.new
      s = i * 9
      (s..s+8).each{|j|
        @yoko_index[i] << j
      }
    }

    @tate_index = Array.new
    (0..8).each{|i|
      @tate_index[i] = Array.new
      (0..8).each{|j|
        @tate_index[i] << (j * 9) + i
      }
    }

    @block_index = Array.new
    block_pattern = [0,1,2,9,10,11,18,19,20]
    (0..8).each{|i|
      @block_index[i] = block_pattern.map{|j|
        (j + (i / 3) * 27) + ((i % 3) * 3)}
    }
  end

  def yoko(x)
    @yoko_index[x].collect{|x| @matrix[x]}
  end

  def tate(x)
    @tate_index[x].collect{|x| @matrix[x]}
  end

  def block(x)
    @block_index[x].collect{|x| @matrix[x]}
  end

  def which_block(x,y)
    ((y / 3) * 3) + (x / 3)
  end

  def index(x,y)
    x + (y * 9)
  end

  def fill_matrix
    srand
    100.times{|i|
      break if self.try_fill_matrix
    }
#    puts self.to_s
# average 7.53 times
  end

  def try_fill_matrix
    count = 0
    abandon_flag = false

    @matrix.fill(0)

    (0..8).each{|y|
      repeat_flag = true
      break if(abandon_flag == true)
      until(repeat_flag == false)
        count += 1
        if (count > 100)
          abandon_flag = true
          @matrix.fill(0)
          break
        end
        seeds = (1..9).to_a
        (0..8).each{|x|
          appear = tate(x) | block(which_block(x,y))
          n = (seeds - appear).pick_one
          @matrix[index(x,y)] = n
          seeds.delete(n)
          if((x == 8) && (!yoko(y).include?(nil)))
            repeat_flag = false
          end
#          puts self.to_s
        }
      end
      }
#    puts self.to_s
    !abandon_flag
  end

  def make_new_puzzle
    self.fill_matrix
    self.reduce
  end

  def reduce
    1000.times{
      delete_one
    }
#    self.to_s
    puts (0..80).to_a.delete_if{|i| @matrix[i] == 0}.size
  end

  def delete_one
    srand
    candidate = (0..80).to_a
    candidate.delete_if{|i| @matrix[i] == 0}
    c = candidate.pick_one
    if(uniq_solution?(c))
      @matrix[c] = 0
#      self.to_s
    end
  end

  def uniq_solution?(n)
    i = @matrix[n]
    x = n % 9
    y = n / 9

    (1..9).to_a.delete_if{|n| n == i}.each{|j|
      if(!tate(x).include?(j) &&
         !yoko(y).include?(j) &&
         !block(which_block(x,y)).include?(j))
        return false
      end
    }
  end

  def to_s
    print "-"*19,"\n"
    (0..8).each{|y|
      i = 0
      yoko(y).each{|n|
        if((i % 3) == 0)
          separator = "|"
        else
          separator = " "
        end
        n = " " if n == 0
        print separator, n
        i += 1
      }
      print "|\n"
      if(((y + 1) % 3) == 0)
        print "-"*19,"\n"
      end
    }
  end
end

class Array
  def pick_one
    r = rand(self.size)
    self[r]
  end
end

#m = Matrix.new
#m.make_new_puzzle
(sudoku.rb)