Inside-Python - lru_cache

functools --- 高階関数と呼び出し可能オブジェクトの操作 — Python 3.7.3 ドキュメント

pythonのlru_cacheはアノテーションとしてユーザの関数をデコレーションすることができ、 その呼び出しを監視し、結果をキャッシュするものである。

この実装はなかなか面白い。

単純なキャッシュアルゴリズムであれば、ハッシュテーブルでよいのだが、lru_cacheはデータのセット時に使われていないデータの追い出し作業が発生する。

つまり、get時には取得されたデータは「使われた」として順位の修正の必要があり、またセット時にキャッシュの限界に達したら最下位のデータを消す必要がある。

https://github.com/python/cpython/blob/master/Lib/functools.py#L496

実装自体はここにあるが、アノテーションのための実装もそこそこあり、またデータが無制限に入る場合、および1件もキャッシュできない場合には面白くないので 一番コアな部分を抽出する。

また、同期処理に関するところ, statsに関するところはコアな部分ではないのでいったん削った。

        def wrapper(*args, **kwds):
            key = make_key(args, kwds, typed)
            link = cache_get(key)
            if link is not None:
                # Move the link to the front of the circular queue
                link_prev, link_next, _key, result = link
                link_prev[NEXT] = link_next
                link_next[PREV] = link_prev
                last = root[PREV]
                last[NEXT] = root[PREV] = link
                link[PREV] = last
                link[NEXT] = root
                return result

            result = user_function(*args, **kwds)

            if full:
                # Use the old root to store the new key and result.
                oldroot = root
                oldroot[KEY] = key
                oldroot[RESULT] = result
                # Empty the oldest link and make it the new root.
                # Keep a reference to the old key and old result to
                # prevent their ref counts from going to zero during the
                # update. That will prevent potentially arbitrary object
                # clean-up code (i.e. __del__) from running while we're
                # still adjusting the links.
                root = oldroot[NEXT]
                oldkey = root[KEY]
                oldresult = root[RESULT]
                root[KEY] = root[RESULT] = None
                # Now update the cache dictionary.
                del cache[oldkey]
                # Save the potentially reentrant cache[key] assignment
                # for last, after the root and links have been put in
                # a consistent state.
                cache[key] = oldroot
            else:
                # Put result in a new link at the front of the queue.
                last = root[PREV]
                link = [last, root, key, result]
                last[NEXT] = root[PREV] = cache[key] = link
                # Use the cache_len bound method instead of the len() function
                # which could potentially be wrapped in an lru_cache itself.
                full = (cache_len() >= maxsize)
            return result

プログラム全体としては大きく二つに分かれ、以下のuser_functionのコールを境に前半はキャッシュの探索、 後半がキャッシュがヒットしなかった場合のキャッシュの保存になっている。

 result = user_function(*args, **kwds)

キャッシュの探索の部分を見てみると、キーの生成とキャッシュの取得後、キャッシュがヒットした場合には、何やらデータ構造をいじっている様子がわかる。

            key = make_key(args, kwds, typed)
            link = cache_get(key)
            if link is not None:
                # Move the link to the front of the circular queue
                link_prev, link_next, _key, result = link
                link_prev[NEXT] = link_next
                link_next[PREV] = link_prev
                last = root[PREV]
                last[NEXT] = root[PREV] = link
                link[PREV] = last
                link[NEXT] = root
                return result

ネタを明かしてしまうと、lru_cacheはハッシュテーブルに加えて、双方向の連結リストを用いてキャッシュを実装している。

f:id:mitsuo_0114:20190529232121p:plain

イメージ的にはこんな感じ。

                # Move the link to the front of the circular queue
                link_prev, link_next, _key, result = link
                link_prev[NEXT] = link_next
                link_next[PREV] = link_prev

まず、この3行ではキャッシュヒットしたデータを持つノードを連結リストの中から取り除く作業をしている。

双方向連結リストでは、自分の一つ前のノードの「次のノードへのポインタ」に対し、自分の次のノードのポインタを渡してあげ、 自分の次のノードの「前のノードへのポインタ」には、自分の前のノードのポインタを渡してあげる。

これによって、自らへの参照を取り除くことができる。

その後、取り除いたノードを一番最後に挿入する。

                last = root[PREV]
                last[NEXT] = root[PREV] = link
                link[PREV] = last
                link[NEXT] = root

rootはダミーオブジェクトであり、双方向連結リストではよく用いられるもので、 一番最後のオブジェクトの次、一番最初のオブジェクトの前に存在するイメージ。

ここでは、rootの前=一番最後のノードを取得し、この最後のノードと、rootの間に入れることで一番後ろにデータを挿入することになる。

これらの作業によって、キャッシュがヒットした場合、そのヒットしたデータを持つノードの順位を変更することが可能になっている。

次に後半の挿入を見てみる。

挿入はキャッシュがヒットしなかった場合に、user_functionの結果を格納するときにおこる。

一番最後の部分を最初に見ると、以下のように新しくデータを作成したうえで、キャッシュへの保存および、双方向連結リストの一番後ろにデータを持ってきていることがわかる。

                # Put result in a new link at the front of the queue.
                last = root[PREV]
                link = [last, root, key, result]
                last[NEXT] = root[PREV] = cache[key] = link
                # Use the cache_len bound method instead of the len() function
                # which could potentially be wrapped in an lru_cache itself.
                full = (cache_len() >= maxsize)

ここの部分が、新しくノードを作っているところ。4つは順番に前のノード、次のノード、キー、データの意味合い。

                link = [last, root, key, result]

最後に、fullであり、追い出しをする場合には以下。

                elif full:
                    # Use the old root to store the new key and result.
                    oldroot = root
                    oldroot[KEY] = key
                    oldroot[RESULT] = result

                    # Empty the oldest link and make it the new root.
                    # Keep a reference to the old key and old result to
                    # prevent their ref counts from going to zero during the
                    # update. That will prevent potentially arbitrary object
                    # clean-up code (i.e. __del__) from running while we're
                    # still adjusting the links.
                    root = oldroot[NEXT]
                    oldkey = root[KEY]
                    oldresult = root[RESULT]
                    root[KEY] = root[RESULT] = None
                    # Now update the cache dictionary.
                    del cache[oldkey]
                    # Save the potentially reentrant cache[key] assignment
                    # for last, after the root and links have been put in
                    # a consistent state.
                    cache[key] = oldroot

コメントを読むに、gcによってデータが意図せぬところで回収されないように参照カウンタに関して少し気を配っているようだが、 やっていることはデータの上書きである。

まず、現在rootを指しているノードに対して、今回のkeyとresultを挿入する。

                    oldroot = root
                    oldroot[KEY] = key
                    oldroot[RESULT] = result

次に、rootの次のノードを指し示し(参照カウンタをキープしたうえで)、ここのデータをNoneに書き換える。

これによって、先ほどまでrootがさしていたノードの次のノードが新しいrootになる。この辺り変数名が親切じゃない気もする。

                    root = oldroot[NEXT]
                    oldkey = root[KEY]
                    oldresult = root[RESULT]
                    root[KEY] = root[RESULT] = None

その後、キャッシュテーブルからも追い出したデータを削除し、新しく追加されたデータをキャッシュテーブルに追加する。

                    del cache[oldkey]
                    # Save the potentially reentrant cache[key] assignment
                    # for last, after the root and links have been put in
                    # a consistent state.
                    cache[key] = oldroot

なかなか興味深い実装で、ここまで有効にデータ構造を使ってるとは思わなかった。

確かに

「現在の最低順位のデータがどれかを知る」、

「任意の位置のデータを最高順位に持ってきて、それ以外のすべてのデータの順位を一つ下げる」

というあたりの要件を考えるにハッシュテーブルと、最初と最後をいじれる双方向連結リストが必要なんだなと思う。

Androidにもlru_cacheという名前の実装があるので今度実装を見てみたい。

Inside-Python - heapqとPriority Queue

最近cpythonのソースコードの中まで見ることが多いので、ネタごとにまとめてみる。

queue --- 同期キュークラス — Python 3.7.3 ドキュメント

heapq --- ヒープキューアルゴリズム — Python 3.7.3 ドキュメント

heapqはPythonの中で、二分ヒープを実装したものであり、競技プログラミングではよくお世話になっている。

二分ヒープ - Wikipedia

二分ヒープは優先度付きキューによく使うことが多いが、実はpythonには”PriorityQueue"というクラスも存在している。 Priority Queueとheapqの関係は、マニュアルにこのように書かれており、heapqが内部的に利用されていることがわかる。

優先順位付きキュー(priority queue)では、エントリは(heapq モジュールを利用して)ソートされ、

実際、PriorityQueueの実装は以下のように非常にシンプルになっており、内部構造は単なる配列で、heappush / heappopを使ってput/getを実装していることがわかる。

from heapq import heappush, heappop

class PriorityQueue(Queue):
    '''Variant of Queue that retrieves open entries in priority order (lowest first).

    Entries are typically tuples of the form:  (priority number, data).
    '''

    def _init(self, maxsize):
        self.queue = []

    def _qsize(self):
        return len(self.queue)

    def _put(self, item):
        heappush(self.queue, item)

    def _get(self):
        return heappop(self.queue)

ただしこれらのput/getは直接は呼び出されず、 親クラスのQueueのget/putで外部用のIFを提供し、同期処理と共にget / putを呼び出す構造になっている。

ここの同期処理はまたまとめたいが、以下のようにqsize / empty / fullといったメソッドはすべてmutex処理をしており、 これらはすべてthreadingモジュールを利用している。

もちろんこれらの同期処理は競技プログラミングには不要なため、PriorityQueueを競技プログラミングで利用する意味はほぼ無い。

以前間違えてこちらを使ったらLTEになったことがあったな。。

import threading

class Queue:

    def __init__(self, maxsize=0):
        self.mutex = threading.Lock()

    def qsize(self):
        with self.mutex:
            return self._qsize()

    def empty(self):
        with self.mutex:
            return not self._qsize()

    def full(self):
        with self.mutex:
            return 0 < self.maxsize <= self._qsize()

heapqへの要素の追加はheappushで実装されている。

以下の通りの実装で、与えられたリストに対し、appendを行うところまでは通常の配列操作と同じだが、 配列操作後、_siftdown を呼び出している。これらはヒープ条件を保つために呼び出されるものである。

def heappush(heap, item):
    """Push item onto heap, maintaining the heap invariant."""
    heap.append(item)
    _siftdown(heap, 0, len(heap)-1)

さらに、_siftdownは以下のようになっている。

# 'heap' is a heap at all indices >= startpos, except possibly for pos.  pos
# is the index of a leaf with a possibly out-of-order value.  Restore the
# heap invariant.
def _siftdown(heap, startpos, pos):
    newitem = heap[pos]
    # Follow the path to the root, moving parents down until finding a place
    # newitem fits.
    while pos > startpos:
        parentpos = (pos - 1) >> 1
        parent = heap[parentpos]
        if newitem < parent:
            heap[pos] = parent
            pos = parentpos
            continue
        break
    heap[pos] = newitem

heappushから呼び出されたとき、_siftdownの引数には、startposは木構造のルート、posは新しく追加されたノードのindexが入っている。

        parentpos = (pos - 1) >> 1

ここはheapの特徴で、親のindexを計算している。>>1は右ビットシフト演算なので÷2と同じ意味合い。

つまり、追加された子のノードから初めて、上にさかのぼって要素を入れ替えていっていることがわかる。

二分ヒープ - Wikipedia

図示はWikipediaを参照。ただし、図はmaxヒープだが、heapqはminヒープであり、一番上に最小値が来ることに注意。

次に要素の削除を見てみる。

以下の通りの実装で、heappushと同じく、pop()を行い、それでリストが空になればそれをそのまま返すようになっている。

しかし、pop()はリストの一番後ろを返す、にもかかわらず最小値が存在するのはheapの一番である。

heappopは最小値を返すことが求められているため、pop()した要素を返して正しく動くのはリストに1件しかなかったときのみである。

2件以上あった時には、この一番後ろの要素を1番前に持ってきて、最初の要素をreturnするようにとっておいたうえで、siftup を呼び出している。 これもsiftdownと同じく、ヒープ条件を保つために呼び出されるものである。

def heappop(heap):
    """Pop the smallest item off the heap, maintaining the heap invariant."""
    lastelt = heap.pop()    # raises appropriate IndexError if heap is empty
    if heap:
        returnitem = heap[0]
        heap[0] = lastelt
        _siftup(heap, 0)
        return returnitem
    return lastelt

では_siftupを見てみる。

def _siftup(heap, pos):
    endpos = len(heap)
    startpos = pos
    newitem = heap[pos]
    childpos = 2*pos + 1
    while childpos < endpos:
        rightpos = childpos + 1
        if rightpos < endpos and not heap[childpos] < heap[rightpos]:
            childpos = rightpos
        heap[pos] = heap[childpos]
        pos = childpos
        childpos = 2*pos + 1
    heap[pos] = newitem
    _siftdown(heap, startpos, pos)

heappopから呼び出されたとき、引数には、posはルートのindexである0が入っている。

最初は単に走査に当たっての値の設定だけである。 childpos に関して、これもheapの特徴で、この演算により、左の子のindexが計算できる。教科書ではヒープは1から始まるため、2*posが左の子になることが多いが、 pythonは0から始めているため、+1が入っている。

    endpos = len(heap)
    startpos = pos
    newitem = heap[pos]
    childpos = 2*pos + 1

同様に右の子は左の子の隣にいるので単に+1すればよい。

        rightpos = childpos + 1

それ以後は、左の子と右の子を比べて小さいほうを上にあげて、さらにその子に同じ処理を続けている。

        if rightpos < endpos and not heap[childpos] < heap[rightpos]:
            childpos = rightpos
        heap[pos] = heap[childpos]
        pos = childpos
        childpos = 2*pos + 1

そして最後に行きついた場所にもともと、一番最後にいた要素を入れる。

    heap[pos] = newitem

最後のこの_siftdownの呼び出しはいまいちわからない、、

    _siftdown(heap, startpos, pos)

以下のコメントが多分これを言っているのだろうが、いまいち理解できない。Knuth, Volume 3を買わなくては。

# The child indices of heap index pos are already heaps, and we want to make
# a heap at index pos too.  We do this by bubbling the smaller child of
# pos up (and so on with that child's children, etc) until hitting a leaf,
# then using _siftdown to move the oddball originally at index pos into place.
#
# We *could* break out of the loop as soon as we find a pos where newitem <=
# both its children, but turns out that's not a good idea, and despite that
# many books write the algorithm that way.  During a heap pop, the last array
# element is sifted in, and that tends to be large, so that comparing it
# against values starting from the root usually doesn't pay (= usually doesn't
# get us out of the loop early).  See Knuth, Volume 3, where this is
# explained and quantified in an exercise.
#

_siftdown以外の図示はこちら

二分ヒープ - Wikipedia

また、同様にheapfiyも以下のように_siftupを呼び出していることわかる。

def heapify(x):
    """Transform list into a heap, in-place, in O(len(x)) time."""
    n = len(x)
    # Transform bottom-up.  The largest index there's any point to looking at
    # is the largest with a child index in-range, so must have 2*i + 1 < n,
    # or i < (n-1)/2.  If n is even = 2*j, this is (2*j-1)/2 = j-1/2 so
    # j-1 is the largest, which is n//2 - 1.  If n is odd = 2*j+1, this is
    # (2*j+1-1)/2 = j so j-1 is the largest, and that's again n//2-1.
    for i in reversed(range(n//2)):
        _siftup(x, i)

なお、以下の演算により、一番後ろにいる子の親が計算できる。0からここまでをreversedすることによって下からヒープ条件を満たしていくことができる。

n = len(x)
range(n//2)

教科書で勉強したことが実際このように実装されているとわかると楽しいよね。

AtCoder Beginner Contest 126

ルール変更でBeginnerになりました。

atcoder.jp

Nの制約がテストケースと違う気がする。 あまり確率の問題は得意ではないが、これはそこまで難しくない。

まずサイコロで初期確率は1 / Nずつになる。 さらにそれぞれの場所から始めたとき、Kを超えるまで連続してコインで面を出し続けなければならない。

何回出し続ける必要があるか、はlog Kなので10000だと14程度なはずなのにこれが通らず、30ぐらいにしたら通った。

def solve(N, K):
    b = 1 / N
    poss = [b for _ in range(N + 1)]
    for n in range(30):
        for i in range(N + 1):
            if i == 0:
                continue
            if (i << n) < K <= (i << n + 1):
                poss[i] = b * (0.5 ** (n + 1))

    return "{:1.12f}".format(sum(poss[1:]))


assert (solve(3, 10) == "0.145833333333")
assert (solve(1, 1) == "1.000000000000")
assert (solve(1, 2) == "0.500000000000")
assert (solve(2, 2) == "0.750000000000")
assert (solve(2, 2) == "0.750000000000")
assert (solve(1, 3) == "0.250000000000")
assert (solve(1, 4) == "0.250000000000")
assert (solve(10000, 10000) == "0.333431365967")
assert (solve(100000, 5) == "0.999973749998")
assert (solve(1, 10000) == "0.000061035156")

if __name__ == "__main__":
    N, K = tuple(map(int, input().split(" ")))
    print(solve(N, K))

最近、問題文の配列が1から始まるとき、配列は0番目を余分に取るようにしてる。こっちのほうがミスが少ない気がするのと、 結局heapの配列上の実装などを考えるとき1から始まることを意識しないとダメなので。

atcoder.jp

これは例示が若干ひっかけ。 同じ色に塗られていれば偶数であるが、偶数であれば同じ色、というわけではない。

何の疑いもなく、単に幅優先探索をすればよい。 colorの決定をvisited代わりにすると実装が楽。

木だから深さ優先探索もいけるかな。

from collections import deque


def solve(N, UVWs):
    vertexes = {}

    for u, v, w in UVWs:
        vertexes.setdefault(u, []).append((v, w % 2))
        vertexes.setdefault(v, []).append((u, w % 2))

    colors = [-1 for _ in range(N + 1)]
    colors[1] = 0
    que = deque()
    que.append((1, colors[1]))
    while que:
        u, c = que.popleft()
        for v, w in vertexes[u]:
            if colors[v] == -1:
                colors[v] = (c + w) % 2
                que.append((v, colors[v]))
    return "\n".join(str(c) for c in colors[1:])


assert (solve(3, [(1, 2, 2), (2, 3, 1)]) == "0\n0\n1")
assert (solve(5, [(2, 5, 2), (2, 3, 10), (1, 3, 8), (3, 4, 2)]) == "0\n0\n0\n0\n0")

if __name__ == "__main__":
    N = int(input())
    UVWs = [tuple(map(int, input().split(" "))) for _ in range(N - 1)]
    print(solve(N, UVWs))

atcoder.jp

これは一瞬。

魔法を使えばある一つのカードの整数が判明する。これによってこの一つと偶数条件で対になっているカードが判明し、連鎖的に対になっているカードがすべて判明する。

つまり1回の魔法により、一つの集合がすべてわかることになるので、UnionFindを使って、集合の数を取ればよい。

珍しく、assert文を一つも書かなかった。

class UnionFind:
    import sys
    sys.setrecursionlimit(100000)

    def __init__(self, n):
        self.parents = [i for i in range(n + 1)]

    def root(self, i):
        if self.parents[i] == i:
            return i
        else:
            self.parents[i] = self.root(self.parents[i])
            return self.parents[i]

    def unite(self, i, j):
        self.parents[self.root(self.parents[i])] = self.root(j)

    def is_unite(self, i, j):
        return self.root(i) == self.root(j)


def solve(N, XYZs):
    uf = UnionFind(N)
    for x, y, z in XYZs:
        uf.unite(x, y)
    return len(set([uf.root(i) for i in range(1, N + 1)]))


if __name__ == "__main__":
    N, M = tuple(map(int, input().split(" ")))
    XYZs = [tuple(map(int, input().split(" "))) for _ in range(M)]
    print(solve(N, XYZs))

Fはジャッジ中なのでまた今度。

Google Code Jam Round1A - Alien Rhyme / Golf Gophers

codingcompetitions.withgoogle.com

Alien Rhyme

接尾辞が一致するペアを可能な限り作る問題。

問題文読むのがつらいが、文字は50文字しかないので、実装自体はそこまで難しくない。

対象となる全Wordの末尾を取得し、2個以上被るものがあればそれを抜けばよい。

この時、末尾が3つ以上被るときどれを取ってよいか、という問題が発生するが、 接尾辞の長いほうから考えることによって、どれを取得してもよい。

なぜならば、ある末尾k文字で一致している3つのワードがあった時、 末尾k-1文字~1文字の間はすべてこれらのワードは同じ末尾の文字列が出てくることになる。 つまり、どれをとっても残るものはこれ以降の末尾の一致を考えたときに全く変わらなくなる。

Nはhiddenでも高々1000だったので、O(N2)ぐらいまでなら大丈夫そうってことで以下の実装に。

from collections import Counter

def solve(N, words):
    ans = 0
    for n in range(50, 0, -1):
        c = Counter(w[::-1][:n] for w in words if len(w) >= n)
        for v, count in c.items():
            if count >= 2:
                ans += 1
                remove_indexes = [i for i, w in enumerate(words) if w.endswith(v[::-1])]
                words.pop(remove_indexes[1])
                words.pop(remove_indexes[0])
    return ans * 2


t = int(input())
for case in range(1, t + 1):
    N = int(input())
    words = []
    for i in range(N):
        words.append(input())
    print("Case #%d: %s" % (case, solve(N, words)))

codingcompetitions.withgoogle.com

Golf Gophers

これも問題文を読むのが大変。インタラクティブ問題。

最大18枚の羽根を持つ風車を設置すると、夜の間にいたずらされて、回転させられてしまう。

回転させられる風車がどれかはわからないが、 1回のいたずらで全員同じ方向で1枚の羽根の分だけ回転させることはわかっており、 すべてのいたずらする人が必ず1回のいたずらをすることがわかっている。

夜の前には0番目の羽根が必ず下を向くように設置し、朝起きたときにわかるのは風車のどの羽が下を向いているかである。

設置する風車と羽根の枚数をインプットし、朝起きたときどの羽が下を向いているかをアウトプットとしてもらったとき、 いたずらしている人数を答える。

ざっと聞いた感じであまりやら結構面倒なことになるのは見えたが、実はVisibleのテストは非常に余裕がある。

ので、「高々100回しかいたずらされないので、1年間毎日18枚の風車を18個設置したらどこかで、いずれの風車も1周していない日があるだろう」という期待のもと試してみたらうまくいった。

Hiddenはまだ解説読んでないな、、

from sys import stderr, stdout


def solve(N, M):
    response = []
    out = " ".join(["18"] * 18)
    for _ in range(360):
        print(out, flush=True)
        stdout.flush()
        response.append(input())
    m = 0
    for line in response:
        m = max(m, sum(list(map(int, line.split(" ")))))

    print(m, flush=True)

    verdict = input()
    if verdict == "1":
        return True
    else:
        return False

T, N, M = tuple(map(int, input().split(" ")))
for case in range(1, T + 1):
    if not solve(N, M):
      break
exit()

Pylons は pythonでやったら、HiddenがどうもTLEになってしまったのでC++で書き直し中。。。

diverta 2019 Programming Contest

残念、Unratedになってしまった。運営の方々お疲れ様です。

atcoder.jp

例のごとく、assertの量がドはまり感を醸し出す。 まず、”AB"が含まれている場合はよい。で、単に末尾のAと先頭のBだけでなく、両方とも満たす場合は別に考える必要がある。

ここからABの数を数えるときに両側にあるものを1列に並べることで、並べた個数をCとしたとき、C-1個の"AB"を作ることができる。

ここまではよかった。では、これと残りの末尾のA、先頭のBをどう組み合わせるかで微妙な勘違いをしてはまった。

回答的には単にCを並べているかどうか、a,bがあるかだけで、判断すればよい。

最後にaのみ、bのみを少ないほうに合わせた数でABができるのでそれを足す。

def solve(N, Ss):
    a = 0
    b = 0
    both = 0
    ans = 0
    for s in Ss:
        ans += s.count("AB")
        if s[-1] == "A" and s[0] == "B":
            both += 1
        else:
            if s[-1] == "A":
                a += 1
            if s[0] == "B":
                b += 1

    if both > 0:
        ans += both - 1

    if both > 0 and a > 0:
        ans += 1
        a -= 1

    if both > 0 and b > 0:
        ans += 1
        b -= 1

    ans += min(a, b)
    return ans


assert (solve(1, ["AB"]) == 1)
assert (solve(2, ["AB", "ABABAB"]) == 4)
assert (solve(2, ["AB", "ABABAB"]) == 4)
assert (solve(2, ["CCB", "CCB"]) == 0)
assert (solve(1, ["CABC"]) == 1)
assert (solve(2, ["CCA", "BCC"]) == 1)
assert (solve(4, ["CCA", "BCC", "CCA", "BCC"]) == 2)
assert (solve(3, ["CCA", "BCC", "CCA"]) == 1)
assert (solve(200, ["CCA"] * 100 + ["BCC"] * 100) == 100)
assert (solve(200, ["CCA"] * 110 + ["BCC"] * 90) == 90)
assert (solve(200, ["CCA"] * 90 + ["BCC"] * 110) == 90)
assert (solve(200, ["ABA"] * 90 + ["BCC"] * 110) == 90 + 90)
assert (solve(200, ["ABA"] * 90 + ["BCC"] * 110) == 90 + 90)
assert (solve(200, ["CCA"] * 110 + ["BAB"] * 90) == 90 + 90)
assert (solve(200, ["CCA"] * 90 + ["BAB"] * 110) == 90 + 110)
assert (solve(2, ["ABA", "BAB"]) == 3)
assert (solve(200, ["ABA", "BAB"] * 200) == 3 * 200)
assert (solve(2, ["ABB", "AAB"]) == 2)
assert (solve(200, ["ABB", "AAB"] * 100) == 200)
assert (solve(2, ["ACA", "BCB"]) == 1)
assert (solve(2, ["ACB", "BCA"]) == 0)
assert (solve(2, ["BCB", "BCA"]) == 1)
assert (solve(2, ["BCB", "ACA"]) == 1)

assert (solve(4, ["BCA", "BCA", "CA", "BC"]) == 3)
assert (solve(6, ["BCA", "BCA", "CA", "BC"] + ["CC", "CC"]) == 3)
assert (solve(4 * 2, ["BCA", "BCA", "CA", "BC"] * 2) == 6)
assert (solve(6, ["BCA", "CA", "BC"] * 2) == 4)

assert (solve(2, ["A", "B"]) == 1)
assert (solve(2, ["AA", "BB"]) == 1)
assert (solve(2, ["AAB", "BB"]) == 1)
assert (solve(2, ["AACB", "BB"]) == 0)
assert (solve(3, ["AACB", "BB", "AA"]) == 1)
assert (solve(300, ["BA", "BA", "BA"] * 100) == 299)
assert (solve(300, ["BABA", "BABA", "BABA"] * 100) == 299 + 300)
assert (solve(400, ["BABA", "BABA", "BABA", "AA"] * 100) == 299 + 300 + 1)
assert (solve(4, ["AA", "BA", "BA", "BB"]) == 3)
assert (solve(3, ["AB", "BB", "AA"]) == 2)
assert (solve(3, ["AB", "BA", "AA"]) == 2)
assert (solve(3, ["AB", "BA", "BA"]) == 2)
assert (solve(4, ["AB", "BA", "BA", "AA"]) == 3)
assert (solve(4, ["AB", "BA", "BA", "BB"]) == 3)
assert (solve(3, ["AB", "BA", "BB"]) == 2)
assert (solve(3, ["AB", "AA", "BB"]) == 2)
assert (solve(2, ["AB", "BA"]) == 1)
assert (solve(2, ["BA", "BA"]) == 1)
assert (solve(3, ["AB", "BA", "BA"]) == 2)
assert (solve(3, ["BA", "BA", "BA"]) == 2)
assert (solve(3, ["BCA", "BCA", "BCA"]) == 2)
assert (solve(3, ["BCC", "BCA", "BCA"]) == 2)
assert (solve(3, ["CCA", "BCA", "BCA"]) == 2)

print("OK")
if __name__ == "__main__":
    N = int(input())
    Ss = [input() for _ in range(N)]
    print(solve(N, Ss))

atcoder.jp

これも約数を求めてカウントすればよい、というところまではすぐに分かったがどうも実装がはまった。

まず、約数を求める範囲だが、

N = m * k + k 

としたとき、例にもあるようにN=8のときm = 7になるため、mを動かしてしまうと調べる範囲がどうも1 - Nの範囲になり間に合わない。

そこで、逆にkを動かそうと考えてみると、

N = m * k + k 
    = (m + 1) * k 

より、一つのkに対して、一つのmが定まることがわかる。

この式を満たすkを探すことを考えれば、考えられるすべてのkを求めるためにはO(√N)ですむ。

(N // k) - 1 = m
def solve(N):
    s = 1
    e = int(pow(N, 0.5)) + 1
    ds = []
    for k in range(s, e):
        if N % k == 0:
            m = (N // k) - 1
            if m and N // m == N % m:
                ds.append(m)
 
    return sum(ds)


assert (solve(8) == 3 + 7)
assert (solve(10) == 4 + 9)
assert (solve(1000000000000) == 2499686339916)
if __name__ == "__main__":
    N = int(input())
    print(solve(N))

Google Code Jam Qualification Round 2019 - Cryptopangrams / Dat Bae

https://codingcompetitions.withgoogle.com/codejam/round/0000000000051705/000000000008830b

疑似的な暗号化問題。

26個の素数が選ばれ、大小関係を維持したまま各アルファベットに割り当てられる。

アルファベットから構成される文章が各文字ごとに対応する素数に変換され、 隣り合う素数との積を取った数列が"暗号文"として生成される。

暗号文のみが与えられたとき、元の文章を求めよ。

なお、素数には2は選ばれず、暗号文にはA-Zすべての文字が含まれることが保証される。

とりあえず素因数分解はすぐに必要だとわかる。

例えば、A = 3, B = 5, C = 7としたとき、 ABCという文章は"15 35"と暗号化されるため、隣り合う暗号文の共通因数を取ることで、 5が使われていることがわかる。

とりあえず共通因数をすべて取れば大体のアルファベットに対応する素数が取れるが、 一番端が計算されていないため、暗号文をさらに各素数で計算してあげればよい。 素数の範囲は大きいが、文字数が少ないためこの手法ができる。

構成される素数がすべてわかったら、変換を行う。 変換には最初の文字が必要だが、これの判別が少し面倒。

平文の1文字目の素数は暗号文の1文字目から構成される素数の中から2つ可能性として出てくる。 これらの候補を次の暗号文の文字から計算すればよいのだが、 この時、以下のように判定に使う暗号文の位置によって文字列の反転されていくことがわかる。

このため、最初の文字を求めるには、「2つの可能性のうちどちらで割り切れるか」だけでは不十分で ここに、「判別に使う暗号文までの距離」が必要。

ABCA 
 => [15, 35, 21]
BABCA
 => [15, 15, 35, 21]
ABABCA
 => [15, 15, 15, 35, 21]

計算的には、 "15 35"で、15と35の間からここで5が使われているとわかったら、最初の文字は15 / 5 = 3であるが、

"15 15 35"で、同様に5が使われているとわかった場合、15 / (15 / 5) = 5が最初の文字となり、

同様に、"15 15 15 35"では、15/ (15 / (15 / 5 )) = 3が最初の文字となる。

もちろん"15 21"のように3が使われていれば、それぞれ逆になる。

これらを含めて偶奇で判断するようにして、最初の文字を判定すれば、そのあとは暗号文を割りながら変換していけばよい。

今思うと候補はたかが2つなのだから両方試して、矛盾が出たほうを切り捨ててもよかったなぁと思う。

import string
import math

def gcd(a, b):
    if b == 0:
        return a
    else:
        return gcd(b, a % b)

def solve(N, L, S):
    Gs = []
    for i in range(L - 1):
        if S[i] != S[i + 1]:
            g = gcd(S[i], S[i + 1])
            Gs.append(g)

    Gs = list(set(Gs))
    while len(Gs) != len(string.ascii_uppercase):
        for s in S:
            for g in Gs:
                c = s // g
                if s % g == 0 and c not in Gs:
                    Gs.append(c)
        Gs = list(set(Gs))

    dec_table = {g: c for c, g in zip(string.ascii_uppercase, sorted(Gs))}

    def find_first(S, table):
        s = S[0]
        for t in table:
            if s % t == 0:
                a, b = t, s // t
                break
        i = 0
        while S[i] == s:
            i += 1
        if S[i] % a == 0:
            i += 1
        if i % 2 == 0:
            return b
        else:
            return a

    index = find_first(S, dec_table)
    ans = [dec_table[index]]
    for s in S:
        index = s // index
        ans.append(dec_table[index])

    return "".join(ans)

t = int(input())
for case in range(1, t + 1):
    N, L = tuple(map(int, input().split(" ")))
    S = list(map(int, input().split(" ")))
    print("Case #%d: %s" % (case, solve(N, L, S)))

実はこの問題、最後のコーナーケースがどうしても見つからなかった。 普段のコンテストだとあまりやらないが、 Qualificationで時間がたっぷりとれたので、自分で乱数生成して、暗号化して正しく複合できないケースを無理やり見つけられたのでよかった。 計算量確認もできるし、時間をかけたらかけただけモンキーテストが試せるので安心。

for _ in range(0, 10000):
    m = gen_map()
    str = string.ascii_uppercase * 10 + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
    str = [c for c in str]
    random.shuffle(str)
    str = "".join(str)
    print(str)
    encrypted = encrypt(str, m)
    assert (solve(0, len(str) - 1, encrypted) == str)

https://codingcompetitions.withgoogle.com/codejam/round/0000000000051705/00000000000881de

Interactiveな問題。これ準備にめっちゃ時間かかった。去年のinteractive問題で試してなかったら多分できなかった。

壊れたworkercomputer群に入力をして、その出力結果から壊れたコンピュータを探す。

F=10だけ解けた。

コンピュータ数は1024までのため、10bitをフルに使って、00000 00000 から 11111 11111を並べ、縦に切り取った数値を投げる。

N=8として3bitで表現すると、以下の中から11110000と00110011と01010101を切り取ればよい。

000
001
010
011
100
101
110
111

これを投げて返ってきたデータを再度並べなおす。3回投げて返ってきた結果が以下だったとする。

000
001
011
100
110
111

これらの数値を10進数変換して、0 ~ Nの間で抜けているものを探せばよい。

F=10だとこれらの並び順の情報を一切使ってないので、F=5の時は並び順の情報も使えば行けるんじゃないかなとは思う。解説はまだ見てない。。

from sys import stderr, stdout

def solve(N, B, F):
    outputs = ["" for _ in range(F)]
    for i in range(N):
        b = "{:010b}".format(i)
        for d in range(F):
            outputs[d] += b[d]

    response = []
    for out in outputs:
        print(out, flush=True)
        stdout.flush()
        response.append(input())

    resdata = ["" for _ in range(N - B)]
    for res in response:
        for i in range(N - B):
            resdata[i] += res[i]

    exists = []
    for res in resdata:
        exists.append(int(res, 2))
    ans = []
    for i in range(N):
        if i not in exists:
            ans.append(str(i))

    print(" ".join(ans), flush=True)

    verdict = input()
    if verdict == "1":
        return True
    else:
        return False

t = int(input())
for case in range(1, t + 1):
    N, B, F = tuple(map(int, input().split(" ")))
    if not solve(N, B, F):
      break
exit()

Google Code Jam Qualification Round 2019 - Foregone Solution / You Can Go Your Own Way

codingcompetitions.withgoogle.com

f:id:mitsuo_0114:20190504234252p:plain

1813位でした。

Foregone Solution

https://codingcompetitions.withgoogle.com/codejam/round/0000000000051705/0000000000088231

任意の数値を二つの和に分ける。ただし、その和は4を使ってはならない。

1桁ずつ、繰上りが発生しないように分けていく。 6以下は、奇数の時に備えてceil / floorを使えば単に半分にすればよい。 7以上はその実装では4が発生する可能性があるので、6を引けばよい。 これで桁ごとに計算が可能なので、計算量はO(logN)になる。

import math

t = int(input())
for i in range(1, t + 1):
  N = int(input())
  a = 0
  b = 0
  for d in str(N):
      a *= 10
      b *= 10
      d = int(d)
      if d >= 6:
          a += 6
          b += d - 6
      else:
          a += math.ceil(d / 2)
          b += math.floor(d / 2)
  print("Case #{}: {} {}".format(i, a, b))

You Can Go Your Own Way

https://codingcompetitions.withgoogle.com/codejam/round/0000000000051705/00000000000881da

これはまじでサンプルのイラストがひっかけ。

N * Nのマス目の一番左上から右下までの経路が与えられる。経路は右か下のみ。 この経路のいずれとも重なり合わない経路を一つ出す。

はじめはサンプルのように、与えられた経路に対して四角形を作るような経路を思い立つが、 既存経路を超えて四角形を作るか、既存経路を超えずに手前で作るかは、そのあとの経路次第で決まってしまうためしばらく悩む。

が、最終的に、一番最初と最後が一番重要であることに気が付く。

最初が右、最後が下で入った場合、左の壁と下の壁に沿った経路は被らないし、 最初が下、最後が右で入った場合も、同様に、上の壁と右の壁に沿えば被らない。

最初も最後も右であった場合、まず左の壁と右の壁に沿っていれば被らない。 どこかで左から右にわたれればよい。 そこで考えると、既存経路の縦と横は同数なのに、最初と最後に右を使っているため、 必ずどこかで下下と連続でつながっているところがあるはずである。ので、これを見つけて左の壁から右の壁に飛び移ればよい。

最初と最後が下であった場合も同様に、上の壁から下の壁に、右右のつながりを見つけて飛び移ればよい。

def solve(N, P):
    if P[0] != P[-1]:
        return P[-1] * (N - 1) + P[0] * (N - 1)
    else:
        t = "E" if P[0] == "S" else "S"
        c = 0
        for i in range(len(P)):
            if P[i] == t:
                c += 1
            if P[i] == t and P[i + 1] == t:
                break

        return c * t + P[0] * (N - 1) + t * (N - 1 - c)

t = int(input())
for case in range(1, t + 1):
    N = int(input())
    l = input()
    print("Case #%d: %s" % (case, solve(N, l)))