summer_tree_home

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

The square chest (Scientific Expedition) - 正方形の数を数えよ

どんな問題?

The square chest
http://www.checkio.org/mission/the-square-chest/

No.1~16までの点があり、隣り合う点と点のいくつかは線で結ばれている。
これらの線で構成された正方形の数を数えよ。

文章で説明するより、説明文の図を見た方がわかりやすいだろう。

引数は、点と点を結ぶ線のリストで、[1,2]なら、点1と点2を結ぶ線となる。
戻り値は、線のリストから作られる正方形の数を返す。

例題:

checkio([[1, 2], [3, 4], [1, 5], [2, 6], [4, 8], [5, 6], [6, 7],
     [7, 8], [6, 10], [7, 11], [8, 12], [10, 11],
     [10, 14], [12, 16], [14, 15], [15, 16]]) == 3
checkio([[1, 2], [2, 3], [3, 4], [1, 5], [4, 8],
     [6, 7], [5, 9], [6, 10], [7, 11], [8, 12],
     [9, 13], [10, 11], [12, 16], [13, 14], [14, 15], [15, 16]]) == 2

どうやって解く?

全体のサイズは固定のようなので、作成できる正方形の数は限られていそうだ。
1x1の正方形が9個、2x2の正方形が4個、3x3の正方形が1個で、合計14個しかない。
だったら、正方形14個すべてをチェックするのが手っ取りばやいかな。

あらかじめ14個の正方形について、含まれる線分を定義しておいて、引数にその線分が含まれていれば、正方形が成立することになる。

例えば、点1,2,5,6から成る正方形なら

[[1,2],[1,5],[2,6],[5,6]]

が、線分リストとなる。
サイズが1x1の正方形は簡単だが、2x2になると線は8本になるので、少しやっかいだ。

正方形の左上の座標(x,y)と辺の長さ(length)から、線分リストを作成する関数を作った。

    def get_no(x, y):
        return y * 4 + x + 1

    def get_square_lines(x, y, length):
        for i in range(length):
            yield get_no(x, y + i), get_no(x, y + i + 1)  # left lines
            yield get_no(x + length, y + i), get_no(x + length, y + i + 1)  # right lines
            yield get_no(x + i, y), get_no(x + i + 1, y)  # top lines
            yield get_no(x + i, y + length), get_no(x + i + 1, y + length)  # bottom lines

1x1、2x2、3x3の正方形(合計14個)の線分リストを作成する。

    squares = [set(get_square_lines(x, y, 1)) for y in range(3) for x in range(3)]  # 1x1 squares
    squares += [set(get_square_lines(x, y, 2)) for y in range(2) for x in range(2)]  # 2x2 squares
    squares += [set(get_square_lines(0, 0, 3))]  # 3x3 square

あとから、引数のリストと比較することを考えて、listではなくsetにしておいた。

まとめたのがこちら

def get_all_squares():
    def get_no(x, y):
        return y * 4 + x + 1

    def get_square_lines(x, y, length):
        for i in range(length):
            yield get_no(x, y + i), get_no(x, y + i + 1)  # left lines
            yield get_no(x + length, y + i), get_no(x + length, y + i + 1)  # right lines
            yield get_no(x + i, y), get_no(x + i + 1, y)  # top lines
            yield get_no(x + i, y + length), get_no(x + i + 1, y + length)  # bottom lines

    squares = [set(get_square_lines(x, y, 1)) for y in range(3) for x in range(3)]  # 1x1 squares
    squares += [set(get_square_lines(x, y, 2)) for y in range(2) for x in range(2)]  # 2x2 squares
    squares += [set(get_square_lines(0, 0, 3))]  # 3x3 square
    return squares

SQUARES = get_all_squares()

def checkio(lines_list):
    lines_set = set(tuple(line) for line in lines_list)  # 比較用に、引数をset of tupleに変換
    return len([sq for sq in SQUARES if sq <= lines_set])  # 引数に含まれる正方形を数える

あらかじめSQUARESという定数を作っておく。
メイン部分(checkio)では、引数に含まれている正方形の数を数えているだけだ。

sq <= lines_set

sq.issubet(lines_set)

と同じで、部分集合かどうかを判定できる。(これを使うためにsetに変換したのだ。)

さくっと実行~

FAIL
Your answer: 0
Right answer: 3
checkio([[16,15],[16,12],[15,11],[11,12],[11,10],[10,14],[9,10],[14,13],[13,9],[15,14]])

えっ、なんで?
引数をよく見たら、

[16,15]
[14,13]

のように、順番が逆になっているものが含まれるじゃないか。想定してなかった。

なら、引数をset of tupleに変換するときに、sorted()しておこう。
(または、tupleじゃなくfrozensetを使ってもいいかも。)

def checkio(lines_list):
    lines_set = set(tuple(sorted(line)) for line in lines_list)
    return len([sq for sq in SQUARES if sq <= lines_set])

これで無事にクリア&公開
http://www.checkio.org/mission/the-square-chest/publications/natsuki/python-3/first/