summer_tree_home

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

Painting Wall (Mine) - 壁のペンキ塗り

どんな問題?

Painting Wall
http://www.checkio.org/mission/painting-wall/

指定された手順に従って、壁にペンキを塗っていく。例えば、手順が[1,5]であれば、壁の1枚目から5枚目までを塗る。同じ壁を重複して塗った場合も1枚として数える。
必要な枚数を塗り終わるまでに、何番目の手順まで進めればよいかを調べよ。

引数は、塗るべき壁の枚数と、手順のリストが与えられる。
戻り値は、指定した枚数を塗り終わるまでの手順の数を返す。すべての手順を終えても、必要な枚数に達しない場合は -1 を返す。

ヒントに、バイナリサーチ(二分探索)を試してみようと書いてある。

例題:

checkio(5, [[1,5], [11,15], [2,14], [21,25]]) == 1 # The first operation will paint 5 meter long.
checkio(6, [[1,5], [11,15], [2,14], [21,25]]) == 2 # The second operation will paint 5 meter long. The sum is 10.
checkio(11, [[1,5], [11,15], [2,14], [21,25]]) == 3 # After the third operation, the range 1-15 will be painted.
checkio(16, [[1,5], [11,15], [2,14], [21,25]]) == 4 # Note the overlapped range must be counted only once.
checkio(21, [[1,5], [11,15], [2,14], [21,25]]) == -1 # There are no ways to paint for 21 meters from this list.
 
checkio(1000000011,[[1,1000000000],[11,1000000010]]) == -1 # One of the huge test cases.

ちなみに、この問題の作者はCielさんで、(たぶん)CheckiOで最初の日本人が作った問題になる。

どうやって解く?

普通に考えると、壁1枚ごとに塗り終わったかどうかのフラグを作成しておいて、手順を1つ進めるごとに塗り終わった枚数を数えれば良さそうだ。

ただし、条件に 2*1018と書かれているのが気になる。普通の解き方ではメモリ不足になりそうな予感がする・・・。ヒントの二分探索も、どう使うのかがよくわからない。

とりあえずコードを書いてみた。手順ごとに、塗り終わった壁の番号をsetに追加していく。

def checkio(num, data):
    painted = set()
    for i, (start, end) in enumerate(data):
        painted |= {n for n in range(start, end + 1)}
        if len(painted) >= num:
            return i + 1
    return -1

ローカル環境で実行してみると、やっぱり例題の最後の問題で Memory Error となってしまった。

    assert checkio(1000000011, [[1, 1000000000], [11, 1000000010]]) == -1, "large"

やっぱり・・・。
さて、どうしようか。

先ほどメモリエラーになった例題を見ていると、

  • 手順1で、1~1000000000(10億)を塗る
  • 手順2で、11~1000000010 を塗る

という作業なのだから、

  • 壁1~10は、手順1のみ
  • 壁11~1000000000は、手順1と2で重複して塗っている
  • 壁1000000001~1000000010は、手順2のみ

の3パターンだとわかる。壁1枚ごとにフラグを設定する必要はなく、このケースならフラグは3つあればいいわけだ。

壁1~10のようなひとかたまりをセグメントと呼ぶことにして、どうやってセグメントのリストを求めればいいだろうか?あれこれ考えて、

  • 手順の後ろの数字を+1にする。手順が[1,5]なら(1,6)
  • 手順リストに出てくるすべての数字を番号順にソート
  • 前から2つずつペアにして、2番目の数字を-1する

とすることでセグメントのリストを得ることができた。

[[1, 5], [11, 15], [2, 14], [21, 25]]
   ↓
(1, 6), (11, 16), (2, 15), (21, 26)
   ↓
[1, 2, 6, 11, 15, 16, 21, 26]
   ↓
[(1, 1), (2, 5), (6, 10), (11, 14), (15, 15), (16, 20), (21, 25)]

まとめ

def checkio(num, data):
    # Make segments
    # ex.
    #   data = [[1, 5], [11, 15], [2, 14], [21, 25]]
    #   segments = [(1, 1), (2, 5), (6, 10), (11, 14), (15, 15), (16, 20), (21, 25)]
    numbers = sorted([n for start, end in data for n in (start, end + 1)])
    segments = [(n1, n2 - 1) for n1, n2 in zip(numbers[:-1], numbers[1:])]
 
    painted = set()
    for i, (start, end) in enumerate(data):
        painted |= {(s, e) for s, e in segments if start <= s and e <= end}
        if sum([e - s + 1 for s, e in painted]) >= num:
            return i + 1

http://www.checkio.org/mission/painting-wall/publications/natsuki/python-3/first/


公開後に見直すと、手順に使われている数字(numbers)を求めるとき、同じ数字が出てくる場合があるので、setで重複を除去してからソートした方がよかったかな。

numbers = sorted([n for start, end in data for n in (start, end + 1)])  # list
   ↓
numbers = sorted({n for start, end in data for n in (start, end + 1)})  # set

 

他の人の答え

bisectという、初めて見るモジュールが・・・。気になるところだが、続きは後日に。