Kickstart Round F 2018 - Problem A. Common Anagrams

Dashboard - Kickstart Round F 2018 - Google Code Jam

あーよかったさくっと解けた。

連続した文字列でのアナグラムを含んだ一致数を数える。

文字数が最大50文字なので、1文字の時、高々50パターン、2文字の時49パターン、、となる。 この状況では、50+1 * 25 < 1300程度にしかパターンができないので、それぞれcollections.Counterでカウント数をとっておき、 A,Bそれぞれでパターンを収集、マッチングさせる。

1300* 1300の力業でも計算量は大した量ではないので実装の楽さを優先して以下のように実装。 ほぼすべてデバッガーの実行時チェックなしで書けたのは良い感じ。

from collections import Counter
def solve(A, B):
    L = len(A)
    acounters = []
    bcounters = []
    for i in range(1, L+1):
        for j in range(0, L-i+1):
            acounters.append(Counter(A[j:j+i]))
            bcounters.append(Counter(B[j:j+i]))
    c = 0
    for a in acounters:
        if a in bcounters:
            c += 1
    return c

読了 - コンピュータシステムの理論と実装

www.oreilly.co.jp

少し前に読んだのだけれど面白かった。

まずは大学ぶりにHDLを使って回路を組む。 NAND回路のみを使ってOR/ANDから始まり、マルチプレクサへ行き、クロックが入ってきてメモリ、レジスタ 最終的には機械語命令の仕様に沿ってALUとレジスタを組み合わせる。

プログラミングは1行ずつ実行されるが、このレイヤーだと1クロック時に すべての結線で同時に論理演算が進むので、デバックするのが大変。ここまでで10時間ぐらい使っただろうか。

機械語を理解するCPUを作ったら、次はアセンブラ。 アセンブラといってもほとんど機械語に直で変換できるので軽くシンボルテーブルと定数あたりを作成すればおしまい。

@0
D=M
@1
D=D-M
@10
D;JGT
@1
D=M
@12
0;JMP
@0
D=M
@2
M=D
@14
0;JMP

これが

0000000000000000
1111110000010000
0000000000000001
1111010011010000
0000000000001010
1110001100000001
0000000000000001
1111110000010000
0000000000001100
1110101010000111
0000000000000000
1111110000010000
0000000000000010
1110001100001000
0000000000001110
1110101010000111

これになる。なんてわからない。

この後、中間コードをアセンブラにコンパイルするコードを書く。 この辺りは演算や関数呼び出しのスタックなどをアセンブラで自分で書く必要があり、コピペでは全く太刀打ちできない。

例えば以下のような7 + 8 を演算する中間コードがある。

push constant 7
push constant 8
add

これを1行ずつ読み込み、以下のようなアセンブラを出力するプログラムを書くわけだ。

@7
D=A
@SP
AM=M+1
A=A-1
M=D
@8
D=A
@SP
AM=M+1
A=A-1
M=D
@SP
AM=M-1
D=M
A=A-1
M=M+D

もちろん単純な演算だけでなく、JMP命令につながるものや関数呼び出しも作る。 以下はフィボナッチ数列のコード。アセンブラは400行を超える。

function Main.fibonacci 0
push argument 0
push constant 2
lt                     // checks if n<2
if-goto IF_TRUE
goto IF_FALSE
label IF_TRUE          // if n<2, return n
push argument 0        
return
label IF_FALSE         // if n>=2, returns fib(n-2)+fib(n-1)
push argument 0
push constant 2
sub
call Main.fibonacci 1  // computes fib(n-2)
push argument 0
push constant 1
sub
call Main.fibonacci 1  // computes fib(n-1)
add                    // returns fib(n-1) + fib(n-2)
return

x86やatomをきちんと学んだわけではないけれど、 特定のレジスタにConstant値を入れて、それを参照しながら別のレジスタやメモリに対してアクセスをかけるアセンブラのコードは 高級言語にはない特有の読みにくさがあり、良い勉強になった。

プログラムコードを中間コードにコンパイルするときには構文解析から始まって一度内部的にXMLを作成したうえで、 これらを中間コードに落とし込む。 このような構文解析と出力の分離はLLVMの階層構造にもつながるところがあり、LLVMというものが流行っているのかなどと考えさせられるきっかけにもなった。

The Architecture of Open Source Applications: LLVM http://www.aosabook.org/images/llvm/LLVMCompiler1.png

実装はなかなかつらかったが、低レイヤーを歩き始めるには非常に良い本であった。

読了 - Unix考古学 Truth of the Legend

アルゴリズムの話だけだと脳がないので読んだ本もつらつらと。 とはいえ時間かけてもしょうがないのでざっと。

ケントンプソンとデニスリッチーの話からUNIXの時代をざっと見る。 最後まで行っても自分は生まれなかった。

正直、Linuxしか触ってない人間にはUNIXなんてMacOSの一部で、SolarisとかIBM AIXとか使っているのは知っている程度。 物語の中に出てくる話はあまりピンとくるところが多くなかった。 それでも、DECに勤めていた人と知り合いだったのでそのあたりの話は興味をもって読めた。

技術面だと、最近ちょこちょこ手を出している低レイヤー部分の実装が どのようにしてOSに組みあがっていったのかが何となく想像できるような内容。 アセンブラが人間の手で組み立てられてコンパイラが出来、コンパイラがコンパイラを作り、 UNIXが物理レイヤーへの依存を吸収したとき、そのときが今の時代のOSの始まりなんだろう。 また、今の時代でも使われて続けている仮想メモリ機構などは、当時の世界最高の大学の博士論文で構成されてる。

歴史的な話だと、独占禁止法によってAT&Tがソースコード開示という戦略を取った時から、オープンソースは始まった。 もちろん最初はオープンソースではなく、AT&Tがコンピュータを主力に置かないという見せしめのようなものだったのだけれども、 コミュニティが立ち、UNIXソースコードが開示されなくなった後は廃れずに、Linux含め、いろいろな方向に飛び火するわけだ。

映画などでノンフィクション物を見ることが多かったり、経営学の勉強をするときに1970年代~80年代の戦略や企業論を知るのだけれど、 その背景でコンピュータの世界がどう動いていったのか、時間軸を持ちながら知識を紐づけて学んでいきたい。

Kickstart Round G 2018 - Problem A. Product Triplets

Dashboard - Kickstart Round G 2018 - Google Code Jam

smallはまわすだけ、largeはむずい。 atcoderでいうと、smallは200点、largeは400点問題ぐらいだろうか。

def small_solve(case):
    nums = list(map(int, case.split(" ")))
    count = 0
    for x in range(len(nums)):
        for y in range(x + 1, len(nums)):
            for z in range(y + 1, len(nums)):
                xn, yn, zn = nums[x], nums[y], nums[z]
                if xn == yn * zn or yn == xn * zn or zn == xn * yn:
                    count += 1
    return count

0を特別扱いするところまでは思いついたが、 x = 0 and y = 0を特別扱いするか、x = 0 or y = 0を特別扱いするかあたりがうまく回らなかったのと、 x, y を定めた後のzをbinary searchしたがまだ遅かった。

from math import factorial as f
def large_solve(case):
    def nCr(n, r):
        return f(n) // f(r) // f(n - r)
    nums = sorted(list(map(int, case.split(" "))))
    counter = {i: 0 for i in nums}
    ans = 0
    for yi in range(len(nums) - 1, -1, -1):
        y = nums[yi]
        for xi in range(yi - 1, -1, -1):
            x = nums[xi]
            if y > 0 and x > 0:
                ans += counter[y*x] if x * y in counter else 0
        counter[y] += 1

    z = counter[0] if 0 in counter else 0
    if z >= 3:
        ans += nCr(z, 3)
    if z >= 2:
        ans += nCr(z, 2) * (len(nums) - z)
    return ans

今回の学び。

  1. ソートすること、探索する計算は減らせる時がある。
  2. 配列の組み合わせでx < y < zにおいて、x, yから計算結果をzに見つけるとき、逆順にすることでsearchをせずにカウントが可能。
  3. pythonのrangeは逆順がとても読みにくいんだがきちんとかけるように。
  4. 特別な場合はダブりに気をつければ、countだけとって別に計算することも出来る。今回の場合は0。

過去のkickstartをとことんやらなくては。

AtCoder Beginner Contest 089 - C

beta.atcoder.jp

知っててよかったitertools.combinations 頭文字ごとにカウントをした後、combinationsでMARCHの中から重複を許さずに3つ取ってくれるというなんて素晴らしい、な関数を使えばよい。 あらかじめ同名は削っておいたが異なるって条件があったことに後から気が付いた。

from itertools import combinations


def solve(N, Ss):
    caps = {c: set() for c in 'MARCH'}
    for s in Ss:
        caps.setdefault(s[0], set()).add(s)
    sum = 0
    for d in combinations('MARCH', 3):
        sum += len(caps[d[0]]) * len(caps[d[1]]) * len(caps[d[2]])
    return sum


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

AtCoder Beginner Contest 088 - C

beta.atcoder.jp

初め、aの存在範囲を勝手に-50 ~ 50としていて総当たりしたが間違いだと気が付く。 あくまでもa + bが0 <= a + b <= 100なのであってこれからa, bの存在範囲は決められない。

では違うアプローチ。 表のすべての値を足すとsum(a1, a2, a3, b1, b2, b3) * 3、というのはすぐわかる。3の倍数が出てきてうれしくなるが、これだけでは必要条件とはならなそう。 sum(a1, a2, a3, b1, b2, b3)がどこかにあるかと探すと、c11, c22, c33があった。 これらが3倍になっていることを確認すると、9つの数値が使われているのでよさそう、で実際よかった。

証明まで持ち込めはしないが直観的にはこれでいいか。

def solve(c1, c2, c3):
    if (c1[0] + c2[1] + c3[2]) * 3 == sum(c1) + sum(c2) + sum(c3):
        return "Yes"
    return "No"


if __name__ == "__main__":
    c1 = list(map(int, input().split(" ")))
    c2 = list(map(int, input().split(" ")))
    c3 = list(map(int, input().split(" ")))
    print(solve(c1, c2, c3))

AtCoder Beginner Contest 085 - C

beta.atcoder.jp

N枚のお札がすべてが5000円だったとして目標金額と合計金額の比較をすると、 合計金額を減らすためには5000円を1000円に変換するしかなく、 合計金額を増やすためには5000円を10000円に変換するしかない。 これを利用すればよい。 もし行ったり来たりしてもたどり着けないならばそれは不可能な数値。

実装上の注意としては、N枚目の変換が終わった後にもう一度判定を入れなくてはならないので、 ループをN+1回回すか、ループ終了後に再度判定を入れる必要がある。サンプルテストケースに助けられたので反省。 また、sumは毎回計算しても全然間に合ったが、本当は5000 * Nの初期値に対して変換のたびに-4000か+5000すれば追加の計算は不要。

もしお札が4枚だったら場合分け必要そうだから探索かな。。

def solve(N, Y):
    r = [5000 for _ in range(N)]
    for i in range(N + 1):
        s = sum(r)
        if s == Y:
            return (
            len([d for d in r if d == 10000]), len([d for d in r if d == 5000]), len([d for d in r if d == 1000]))
        elif s < Y and i < N:
            r[i] = 10000
        elif s > Y and i < N:
            r[i] = 1000
    return (-1, -1, -1)


if __name__ == "__main__":
    N, Y = tuple(map(int, input().split(" ")))
    print(" ".join(map(str, list(solve(N, Y)))))