sudoku高速化

sudoku.rbを高速化してみた。マトリックスを埋めたあとに、1つずつ数字を消す処理(reduce+delete_one)を変えた。チェック済みかどうかを確認するのが面倒だったので、単純に1000回ランダムチェックを繰り返して「まあ大体調べたことになるだろう」という、すさまじい富豪アプローチだったのをやめて、ちゃんと1セル1チェックにした。それだけで100問の生成時間が110秒から30秒へと3.6倍に高速化した。さらに、マトリックスの各列を埋めるときに何回ぐらいのリトライで「やっても無駄」と判定するかの閾値を100回から30回に減らした(これを10回に減らせると規定回数のうちに1つもマトリックスが生成されないケースが出てくる)。これで約3倍のスピードアップ。

合計10倍の速度アップ。110秒かかっていたものが10秒に縮んだ。Ruby 1.9だと実に4.7秒。表示を見ていると鬼のように速い感じがする。元の計算方法がひどすぎただけなのだけど。

#!/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)
=end

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

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

    @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 > 20)
          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
        }
      end
      }
    !abandon_flag
  end

  def make_new_puzzle
    self.fill_matrix
    self.reduce
  end

  def reduce
    srand
    candidate = (0..80).to_a
    candidate.delete_if{|i| @matrix[i] == 0}
    
    while(candidate.size > 0)
      c = candidate.pick_one
      if(uniq_solution?(c))
        @matrix[c] = 0
      end
      candidate.delete(c)
    end
    self.to_s
    puts (0..80).to_a.delete_if{|i| @matrix[i] == 0}.size
  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)