テトロミノビンゴ

テトロミノビンゴ
CodeIQ に出した「テトロミノ+ビンゴ!」の解説・解題
にあった問題を解いてみた。
言語は Ruby
リンク先にある data.txt が入力データとして必要。
手元の環境での実行時間は1.1秒程度。Ideone.com 程度の処理速度なら1秒未満のはず。

カードの状態は1次元の配列(プログラム中では mark という変数)で持つことにして、カードの端での条件判定をしなくてすむように、添え字とマス目の対応は下図のようにした。

 6  7  8  9 10
12 13 14 15 16
18 19 20 21 22
24 25 26 27 28
30 31 32 33 34

テトロミノの種類の判定は、テトロミノを構成するマス目を昇順に c_0,\; c_1,\; c_2,\; c_3 としたとき、
[c_1-c_0,\; c_2-c_0,\; c_3-c_0] をキーに持つハッシュ (@tetromino) を参照することで行っている。

find_tetromino がマークのついたマス目の塊を探しているところ。
mark[cell] = cell とか mark[d] = cell とかいう書き方が一見変だが、要するに cell からつながっているマス目に cell (の値)で印をつけていっている。mark[d] == cell で、マス目 d が検査済みかどうかを判定できるようにこうした。group.member?(d) と判定するより少し速い。
塊の大きさは group.size で見ているが、塊の大きさを持つ変数を別に作るよりかすかに速い。
(配列やハッシュのメソッドより自前のコードのほうが速そうと思って余計なことをすると、裏目に出ることが多い気がする。)
return if group.size > 4 はその下の end の外側に出せるが、この位置のほうがかすかに速い気がする。

class TetrominoBingo
  # テトロミノの種類の判定用のデータ @tetromino を最初に作っておく
  def initialize
    tetromino_data = [
      [:I, [0,0], [0,1], [0,2], [0,3]],
      [:L, [0,0], [0,1], [0,2], [1,2]],
      [:O, [0,0], [0,1], [1,0], [1,1]],
      [:S, [0,0], [0,1], [1,1], [1,2]],
      [:T, [0,0], [1,0], [2,0], [1,1]]
    ]
    @tetromino = {}
    tetromino_data.each do |name, *coord|
      4.times do
        @tetromino[coord_to_cell(coord)] = name
        # 反転
        @tetromino[coord_to_cell(coord.map {|x, y| [-x, y]})] = name
        # 回転
        coord.map! {|x, y| [-y, x]}
      end
    end
  end

  # initialize の下請け
  def coord_to_cell(coord)
    cells = coord.map {|x, y| x + (N + 1) * y} .sort
    top_left = cells.shift
    cells.map {|cell| cell - top_left}
  end

  def group_to_tetromino(group)
    top_left = group.sort!.shift
    @tetromino[group.map {|cell| cell - top_left}]
  end

  # cell からつながるテトロミノが見つかったら、その種類を返す
  # 見つからなければ nil を返す
  def find_tetromino(mark, cell)
    mark[cell] = cell
    group = [cell]
    k = 0
    while c = group[k] do
      [c-1, c+1, c-N-1, c+N+1].each do |d|
        next if not mark[d] or mark[d] == cell
        mark[d] = cell
        group << d
        return if group.size > 4
      end
      k += 1
    end
    return group_to_tetromino(group) if group.size == 4
  end

  def prize(card, nums)
    mark = Array.new((N + 1) * (N + 2))
    nums.each do |n|
      next unless pos = card.index(n)
      prz = find_tetromino(mark, pos + pos/N + N + 1)
      return prz if prz
    end
    :-
  end
end

N = 5
tet = TetrominoBingo.new
file = open('data.txt')
nums = file.gets.split(',').map(&:to_i)
count = {:I => 0, :L => 0, :O => 0, :S => 0, :T => 0, :- => 0}
count[tet.prize($_.split(/[,\/]/).map(&:to_i), nums)] += 1 while file.gets
file.close
p count