summer_tree_home

Check iOでPython3をマスターするぜっ

Radiation search (Electronic station) - N*Nのマスをグループ分け

どんな問題?

Radiation search
http://www.checkio.org/mission/radiation-search/

N*Nのマスに、それぞれNo.1~5の数字が書いてある。同じ数字が縦横につながっている部分をグループとして、もっとも大きなグループのサイズ(マスの数)と数字を求めよ。
3 <= N <= 10 とする。

答えは、サイズと数字のリストで返す。No.3が10個のグループなら、 [10, 3]

なんか問題がわかりにくいけど、例えば、

1 1 2 2
1 1 2 1
1 1 2 1
3 3 1 1

この場合だと、

  • No.1が6個のグループ
  • No.2が4個のグループ
  • No.3が2個のグループ
  • No.1が4個のグループ

の4グループあって、最大のグループは、No.1が6個のグループだから、答えは [6, 1] となる。
グループは縦と横にはつながるが、斜めにはつながらない。単純に1~5のマスの数を数えればいいという問題ではないので注意。

例題:

checkio([
 [1,2,3,4,5],
 [1,1,1,2,3],
 [1,1,1,2,2],
 [1,2,2,2,1],
 [1,1,1,1,1]
])==[14,1]

どうやって解く?

まず、同じ番号のグループを探索する部分を考えてみる。開始マスから上下左右が同じ数字ならさらに探索、別の数字ならストップする。なんか、先日の迷路探索にも似ている。探索候補マスをスタックに入れていけばいいか。

1つのグループの探索が完了すれば、次は未探索のマスから開始して別のグループを探索する。こうやって全てのグループをリストアップし、最大のグループを求めればよい。

まず、マトリックスからグループを探索する部分を書いてみた。調査候補マスをスタックに入れていき、スタックが空になればループを抜ける。

import itertools

def get_group(matrix, start, visited):

    def get_number(p):
        # 座標pの数字を取得
        return matrix[p[1]][p[0]]

    def in_matrix(p):
        # 座標pがマトリックスの範囲内ならTrue
        n = len(matrix)
        return 0 <= p[0] < n and 0 <= p[1] < n

    checked = visited + [start]  # 調査済みフラグ
    stack = [start]  # 次に調査する座標(スタック)
    number = get_number(start)

    pts = []
    while len(stack) > 0:
        pt = stack.pop()
        pts.append(pt)
        # 上下左右のマスを調査
        for d in ((0, -1), (0, 1), (-1, 0), (1, 0)):  
            new_pt = pt[0] + d[0], pt[1] + d[1]
            if in_matrix(new_pt) and new_pt not in checked:
                checked.append(new_pt)
                if get_number(new_pt) == number:
                    stack.append(new_pt)

    return len(pts), number, pts

start座標が開始位置で、startと同じ数字のグループを探索して、(サイズ, 数字, 座標リスト) のタプルを返す。
引数のvisitedは、調査しなくて良い座標のリスト。次回探索時に、今回のグループの座標リストを指定しておけば、無駄な調査が減らせるので、少しは高速化できるはず。

メイン部分では、グループの探索と、未探索座標の取得を繰り返して、すべてのグループをリストアップする。

def checkio(matrix):
    def get_unvisited_pt():
        # 未探索の座標を取得
        all_pts = itertools.product(range(len(matrix)), repeat=2)
        unvisited = [p for p in all_pts if p not in visited]
        return unvisited[0] if unvisited else None

    pt = (0, 0)
    groups = []
    visited = []
    while pt is not None:
        size, number, pts = get_group(matrix, pt, visited)
        groups.append([size, number])
        visited += pts
        pt = get_unvisited_pt()

    return max(groups, key=lambda g: g[0])

一応完成したが、どうも無駄な探索が多いような気がする。
メイン部分checkio()のvisitedと、get_group()のcheckedは、どちらも探索済みフラグだから、まとめられないものだろうか。
あれこれ修正してみたが、結局あまり変わらなかったので、このまま公開する。

他の人の答え

あ、

return max(groups, key=lambda g: g[0])

この key= の部分は不要だった…。

さて、他の人の答えを見ると、スタックではなく再帰で書いている人が多かった。

def checkio(l):
    n = len(l)
    flags = [[0 for i in xrange(n)] for i in xrange(n)]
    def search(x, y, value):
 
        if not (0 <= x < n) or not (0 <= y < n):
            return 0
        if flags[x][y]:
            return 0
        current = l[x][y]
        if current == value:
            flags[x][y] = 1
            res = 1
            res += search(x-1, y, current)
            res += search(x+1, y, current)
            res += search(x, y-1, current)
            res += search(x, y+1, current)
            return res
        return 0
 
    res = max([search(x, y, l[x][y]), l[x][y]] for x in range(n) for y in range(n))
    return res

http://www.checkio.org/mission/radiation-search/publications/yumax/python-27/first/

これなんて、とてもシンプルでわかりやすい。
すべての座標についてグループ探索をしているので、すごく無駄のように見えるけど、そこはflagsで探索済みのマスは再調査しないから大丈夫。
なるほど。