HACK TO THE FUTURE 2023 本戦オープン参加記(7位)

atcoder.jp 1,297,045点でオープン7位でした。(点数を得た参加者は83人)。

ビジュアライザも使いやすくて楽しいコンテストでした。問題が面白くて、レートも賞金ももらえないのに6時間くらいやってしまいました。

問題概要

Dowsing Rodという問題でした。

円形の島の中を探索して宝を探します。ある点を調査すると半径D以内に宝があれば発見、なければ距離に反比例する確率で選ばれた未発見の宝の向きが誤差付きで分かるので、できるだけ少ない回数で全部見つけなさいという問題です。

インタラクティブで情報が隠されていて誤差もあるタイプの問題です。HTTFっぽい感じがしますね。

ビジュアライザはこんな感じ。

方針

  • ランダムに点をうち、その方向に一定歩幅で探索を進めていく
    • 初期点の決め方
      • 発見した宝が半分以下の場合は円周上のどこかからスタートする
      • 発見した宝が半分以上の場合は円の内側半径9割のうちどこかからスタートする。
    • 向きが逆転したら二分探索の要領で歩幅を0.5倍する
    • 直前の点よりもその前の点のほうが宝に近かった(1> <3 <2 のような)パターンが現れたら、同じ位置に戻るのを防ぐために0.8倍する
      • 0.5倍より0.8倍の方がスコア出るんですね
    • 歩幅が一定以下になると探索打ち切りでランダムに点を打つところからやり直す
  • 逆転判定の範囲は標準偏差が大きいほうが広がるようにした

悪いケースでも全ての宝を集められるくらいのプログラムになりました。

実装

特に面白い実装はありませんが、各関数が更新する変数は少ないので読みやすいはず。

atcoder.jp

試したがうまくいかなかった方針

  • 円周上に100個の点を打つと直線が100本得られるので、交点の座標を求めて近いものをunionfindする。誤差の前に無力でした。
  • 直前の点と大きく向きが違う場合に無視する。手損でした。

『CODINGAME SPRING CHALLENGE 2021』参加記(Gold 1425th)

CODINGAME SPRING CHALLENGE 2021に参加しました。何も分からん。

コンテストについて

コンテストはこちら。

www.codingame.com

ルール

ルールは、いなにわさんのブログが詳しいです。翻訳ありがとうございます。

inaniwa.hatenablog.com

結果

1652位/6867人でGold、日本ランキングでは141位/424人でした。

f:id:matsu7874:20210518000355p:plain
順位

ビームサーチを実装しました。

下記の State を持って幅100のビームサーチ(深さ17程度)をしました。相手の手番は考慮していません。

struct State {
    day: usize,
    nutrients: i32,
    score: i32,
    sun_point: i32,
    opp_sun_point: i32,
    n_my_trees: [i32; 4],
    sizes: [i32; N_CELLS],
    is_mines: [bool; N_CELLS],
    is_dormants: [bool; N_CELLS],
}

評価関数

盤面の評価はスコアと現在の木配置のまま終局した場合の獲得SunPointを使っています。

遷移先の限定

序盤に木を切ってしまいSunPoint不足になることが多かったため、ゲーム開始から12ターン(太陽が2周するまで)は相手が COMPLETE をしない限りこちらも COMPLETE をしないようにしました。

目下問題に感じていること

下記は対戦を見ていて課題に感じましたが、改善できませんでした。

  • 序盤で種を植える数が少ない
  • 中盤で木を切りすぎる
  • 終盤で木を育てない

その他やったこと

MCTSを試してみたが、ビームサーチより強い実装ができませんでした。

感想

2週間分の土日をほぼ全て費やして参加しましたが、Goldに入って以降、有効な改善が思いつきませんでした。 TLを見る限り、DUCTというアルゴリズムを勉強しておくと良さそうでした。

twitter.com

秋のコンテストに向けて復習をして、下記2冊読んでみようと思います。

天下一 Game Battle Contest 2021 Springで24位でした。

KLab主催の天下一 Game Battle Contest 2021 Springで24位でした。 github.com

すべてのタスク2つ組について、共通なsuffix・prefixを削除して連結した文字列を考え、単位時間あたりの内部に含まれているタスクを考慮した合計得点が最大の連結タスクを全てのbotで行いました。

きれいなビジュアライザが用意されており楽しいコンテストでしたが、 考えをまとめられず、不完全燃焼感が残ります。

最序盤

  • ルールを読みました。
    • ゲーム中APIを叩き続けてもよいことを確認しました。
  • サンプルコードを動かしました。
  • ビジュアライザを見ました。
    • 10人部屋なのに5人しか居ないな……などと思いながら、
  • ルールを読みました。
    • 得点計算の方法
    • 1人5bot
    • 開始50分以降に高得点のタスクが入ってくる

開始50分で高火力のbotを作るコンテストという風に考えました。

序盤

サンプルプログラムをベースに改修しました。

  1. タスクを所要時間あたりの重みが最大のものを使うようにしました。
  2. 同じ文字が連続していた場合に中央に戻らず、x+1を経由するようにしました。
  3. タスクを重みではなく、所要時間あたりの得点が最大のものを使うようにしました。
  4. 自分の他のbotが狙っている分を考慮に入れました。
def estimate_task_score(task: object) -> float:
    before = task['weight'] * (task['count']+task['aim']) / max(task['total']+task['aim'], 0.001)
    after = task['weight'] * (task['count']+task['aim']+1) / max(task['total']+task['aim']+1, 0.001)
    return after-before

f:id:matsu7874:20210424185211p:plain

中盤

タスクの文字列は重なっても良いというルールに気が付きました。

combine(tasks[i]['s'], tasks[j]['s']) の中にどれだけのタスクが含まれているかを計算し、連結したタスクごとに移動時間あたりの得点が最大になる連結タスクを選びました。

def combine(s: str, t: str):
    len_s = len(s)
    len_t = len(t)
    for k in range(min(len_s, len_t)-1, -1, -1):
        if s[-k:] == t[:k]:
            return s + t[k:]
    return s+t

残り1時間時点で10位に入っていました。

終盤

  • 特に改善が思いつかないため、うどんを食べに行きました。

結果

  • 最終的に24位まで順位が下がりました。

SRM788 Div2

2020/07/24にTopcoderで行われたSRM788の参加記です。

公式解説はTopcoder Single Round Match 788 Editorials | Topcoderです。

Easy NextOlympics

"YYYY.MM.DD" 形式で今日の日付が与えられるので東京オリンピックの開催予定日である"July 23rd, 2021"までの日数を出力する問題です。

実装するだけですが、datetime ライブラリを使うより独自実装したほうが早いと思ったため、datetime ライブラリを使わずに実装しました。 ある月のX日からその先月のX日までの日数は先月が何月かに依存して決まるのですね。

class NextOlympics:
    def countDays(self, today):
        y, m, d = today.split('.')
        y = int(y)
        m = int(m)
        d = int(d)

        ty = 2021
        tm = 7
        td = 23
        total = 0
        if y < 2021:
            total += 365 * (ty-y)
        while m < tm:
            total += [0, 31, 28, 31, 30, 31, 30][m]
            m += 1
        while tm < m:
            total -= [0, 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][m]
            m -= 1
        if d < td:
            total += (td-d)
        else:
            total -= (d-td)

        return total

Medium StarsInTheSky

2次元平面上の点集合が与えられるので、軸に平行な矩形で、異なる点集合を含むものを数え上げる問題です。

点の数 N <= 15 なので、総当りを行いました。矩形の判別は左下、右上の頂点対で行いました。

import itertools


class StarsInTheSky:
    def countPictures(self, N, X, Y):
        if N <= 2:
            return [0, 1, 3][N]
        count = set()
        for i in range(2, N+1):
            for p in itertools.combinations(range(N), r=i):
                xs = [X[c] for c in p]
                ys = [Y[c] for c in p]
                count.add(self.getPic(xs, ys))
        return len(count) + N

    def getPic(self, xs, ys):
        max_x, max_y = -float('inf'), -float('inf')
        min_x, min_y = float('inf'), float('inf')
        for x in xs:
            max_x = max(max_x, x)
            min_x = min(min_x, x)
        for y in ys:
            max_y = max(max_y, y)
            min_y = min(min_y, y)
        return (max_x, max_y, min_x, min_y)

Hard largestGCD

2次元グリッド上で (0,0) から (N-1,N-1) までの最短経路のうち、経路上の各頂点の最大公約数(GCD: Greatest Common Divisor)の最大値を求める問題です。

左上から右下に DPするだけ に見えます。

class SquareCityWalking:
    def largestGCD(self, N, A):
        best = [1]*(N*N)
        best[0] = A[0]
        for i in range(1, N):
            best[i] = self.gcd(best[i-1], A[i])
        for i in range(1, N):
            best[N*i] = self.gcd(best[N*i-N], A[N*i])
        for i in range(1, N):
            for j in range(1, N):
                a = A[N*i+j]
                best[N*i+j] = max(self.gcd(best[N*i+j-N], a),
                                  self.gcd(best[N*i+j-1], a))
        return best[N*N-1]


    def gcd(self, a, b):
        while b != 0:
            a, b = b, a % b
        return a

です。解説にある通り、途中で小さい方の値を保持する必要があるケースでWAします。

import unittest
class Test(unittest.TestCase):
    def test(self):
        scw = SquareCityWalking()
        N = 3
        A = [6, 2, 1, 3, 6, 2, 1, 2, 6]
        expected = 2
        self.assertEqual(scw.largestGCD(N, A), expected)

解になる可能性のある値を大きい順にループしながら、 その値を約数に持つマスだけを通って(0,0) から (N-1,N-1) まで最短経路で到達できるか検証するのが想定解でした。 下記がPythonによる実装です。

class SquareCityWalking:
    def largestGCD(self, N, A):
        divisors = []
        for i in range(1, int(A[0]**0.5)+1):
            if A[0] % i == 0:
                divisors.append(i)
                divisors.append(int(A[0]/i))
        divisors.sort(reverse=True)

        for d in divisors:
            visitable = [False]*(N*N)
            visitable[0] = True
            for i in range(1, N):
                visitable[i] = visitable[i-1] and A[i] % d == 0
            for i in range(1, N):
                visitable[N*i] = visitable[N*i-N] and A[N*i] % d == 0
            for i in range(1, N):
                for j in range(1, N):
                    a = A[N*i+j]
                    visitable[N*i+j] |= visitable[N*i + j - N] and a % d == 0
                    visitable[N*i+j] |= visitable[N*i + j - 1] and a % d == 0
            if visitable[N*N-1]:
                return d
        assert False, "unreachable"

確認として求めた値で経路が復元できることを確かめたのですが、それ以上の値で経路が存在しないことを確認する必要がありました。

感想

SRMには久しぶりに参加しましたが、とても楽しいコンテストでした。どの問題もサンプルが強く、これ通ったらchallengeやること無いのでは?と思っているところに上記の落とし穴がありました。

今回のコンテストでレーティングは1197->1229と上昇しました。

ちなみにTopcoderには下記リンクのARENAから参加することが出来ます。興味がある方はぜひ参加してみて下さい。 www.topcoder.com

CODINGAME SPRING CHALLENGE 2020で初Silverを獲りました。

"CodinGame Spring Challenge 2020"に参加しました。

CodinGameにはリーグという階級分けの制度があり、各リーグのBOSSを基準にWood2, Wood1, Bronze, Silver, Gold, Legendと登っていきます。今回のコンテストで初めてSilver(の上位)まで到達することができました。

作成したAIも自分より強い敵にぶつかりに行ったり、同じ場所を往復したりはしない状態になりました。

f:id:matsu7874:20200518204305p:plain
Gloval rank 702/4976、Silver rank 57/999と満足のいく結果となった。

参加経験

CodinGameへの参加は2018年のXMAS RUSH, 2019年のUNLEASH THE GEEKに続いて3度目の参加で、今回のコンテストで初めてSilver Leagueへの進出を果たしました。

f:id:matsu7874:20200518205701p:plainf:id:matsu7874:20200518205658p:plainf:id:matsu7874:20200518205656p:plain
これまでに参加したコンテスト

取り組み

2020/05/08から2020/05/18にかけてCodinGameで開催されていました。ルールは2日目に確認したものの、実装は主に最後の週末に取り組むことになりました。 前回まではPythonで参加していましたが、速度の問題がありますので、今回はRustで実装してみました。トータルで1000行程度のプログラムになりましたが、Rustの型推論のおかげで快適にコーディングができました。

ルール

コンテスト内容はパックマンにじゃんけんを付け足したようなゲームが与えられるので、より強いパックマンのAIを作ろうというものでした。 詳しいルールはツカモさん (@tsukammo)の翻訳記事『codingame:Spring Challenge 2020 ルール要約&マップ生成ロジック説明 - tsukammoの収穫記』を御覧ください。

やったこと

10点のSuper Pelletがあれば優先的に狙い、それ以外は見えているPellet、存在が不明なPelletの順に優先しながら目的地を決定するようにしました。見えているPelletを使ってでできるだけ長い道を通りたい気持ちです。 衝突は自分が死ぬ可能性がある場合のみ移動やスキルを使って全力で避け、それ以外は気にしない実装をしました。また基本的には倍速のスキルを連発するようにしました。

前処理

  • 幅優先探索で各マス間の距離を計算
  • 敵の位置を初期化

毎ターン

  • 10点のSuper Pelletがあれば最も近いPacに向かわせる
    • 各Super PelletとPacの距離をheapに入れて、出てきた順に採用
  • Super Pelletに向かっていないPacは、それぞれのPelletへの経路上のPellet数を数え、多い道に進む。
  • 接敵している場合はSwitchのスキルを検討する
  • 交差点での不慮の事故と袋小路に閉じ込められる以外で死なないように近くの敵からの間合いを調整する
    • 直前のターンに見たが、視界から消えた敵はそこから最大1マス離れているとして考慮

発見した問題・バグと実装

  • HとWを間違える
    • index overflowとして発現
    • 座標を配列で管理する際に y*W + x = i とせず、y*H + x = i と実装していた
  • 殺されがち
    • 自分がSkillを使えないときは逃げる
    • 自分がSkillを使えるときは、次の行動で襲ってくるだろうというタイミングで相手に勝てる手にSwitch
  • Pac同士の目的地が交差するため、お互いが動けずにいた
    • 遷移先の座標を早い者勝ちで管理した
  • 視界から消えた敵の情報が更新されず、同じ場所に留まっているように見えていた、また倍速状態が続いていた
    • 最後に見た時刻を管理した
  • Super Pelletに向かって相手の後ろを走り続ける
    • 相手の方が近ければ諦めるようにした
  • 交差点で相手と衝突し続ける(敵の位置は見えない)
    • 想定する位置を次ターンに持ち越し、想定通りに移動できたか確認する
    • (衝突したターン数/10)+1個強い手にSwitchする

以上を実装したところで体力の限界となり、Goldには届かずという結果になった。Silver内の日本人ランキングでは4位でした。

f:id:matsu7874:20200518204308p:plain
Silver Leagueの日本人ランキング

感想

第一に楽しかったです。ちょうど週末を2回挟むスケジュールなので出やすいですし、徐々に強いAIを倒せるようになる楽しさがありました。 また、今回はPac単位での貪欲しかかけていないので、次回までに評価関数を作って手番全体で最適化できるような実装を練習しておきたいところです。対戦ありがとうございました。

フォルシアと競技プログラミングと私

フォルシアと私

私がフォルシアで働き始めたのが2016年なので、もうすぐ丸4年が経ちます。 キャッチコピーは「見つからなければ、始まらない」から「見つけることから、始めよう。」に変わり、ビジネスも「検索」の受託からより大きな「意思決定のサポート」という領域に広がってきています。 社員数も約120名と私が面接を受けていた頃の2倍近くになりました。(まだ全員の名前を覚えられています。)

就活時は技術力が必要で裁量を持って働ける会社がいいなと思っていて、先輩に勧められたGoodfindに紹介してもらってフォルシアを知りました。 説明会に1回、面接に3回、大学3年生の1月には内定を頂いて、就職活動を終えました。 大学は政治経済学部だったのですが、検索へのモチベーションの高さを説明したり、応用情報技術者試験に合格していたり、code thanks festivalで6位を取ったり、ProjectEulerを解いていたのが良かったのかもしれません。 同期の学歴の高さや人間性の素晴らしさを鑑みるに、即戦力の変な子枠だったのだろうと思っています。

検索は技術的にもアルゴリズムの改善や定数倍高速化、クエリに対する回答ランキングの精度改善(これはレコメンド領域にも広がっていく)と面白いところがたくさんありますし、 これまでは見つけられなかったものが、自分が書いたプログラムによって見つけられるようになるという、間違いなく人類に貢献していると感じられるところが良いです。 もちろんこんな具体的なことは大学生の時には考えておらず、「面接でしっかりと話を聞いてくれ、優しいし賢い人達で一緒に働いたら楽しそうだな、競プロのスキルを活かしてプログラムを書いて、いいお給料がもらえたら嬉しいな。「これまでの不可能を可能にする」ってめっちゃかっこいいやん。」と思っていました。

競技プログラミングと私

競技プログラミングと私の出会いは高校2年生の時で、廊下に張られたSuperConのポスターがきっかけでした。 一番簡単な問題が2*1のタイルをn*2に敷き詰める敷き詰め方を数えるプログラムを書けというもので、当時フィボナッチ数列が再帰関数で書けて、メモ化できてということを知らなかったので衝撃的でした。 中学生くらいからホームページを作ったり、簡単なゲームを作ってみたりしていましたが、それまではプログラミングは何かを動かすための手続きを書くものに過ぎず、同じ結果を求めるのにこんなにも速度の差があるものなのかと驚きました。 和歌山でも蟻本が売っていてお小遣いで買ったのですが、難しすぎて理解できずという感じでした。(※最近の任意の高校生は私より優秀ですね。)

その後、大学生になる頃に自分のPCを買ってもらったり、AtCoderで日本語コンテストがあるぞということで徐々に競プロにのめり込んでいきました。 競技プログラミングに出会っていなければおそらくプログラミングを仕事にしようと思わなかったでしょうし、会社も私を雇わなかったでしょう。

フォルシアと競技プログラミング

2016年に私がフォルシアで働き始めてから行ってきた、競技プログラミングの布教活動と関連した採用活動について時系列で整理してみました。

ある程度、社内にあった高速検索の技術と競技プログラミングの文脈をつなげることに成功したのかなと思っています。 4月にはサマーインターン1期生が入社して来てくれるので一緒に働くのが楽しみですね。

4年働いてみてどうですか?

概して最高に近い環境です。上記のような取り組みもさせてくれますし、アルゴリズムの改善や競プロで身につけた考え方が使える仕事も他の会社よりは多いだろうなと思っています。

まず、フィロソフィーを読んで、ポルカドットスティングレイの『女神』を聞いて欲しい。


ポルカドットスティングレイ「女神」MV

絶対的にここにしか居られないわけじゃない 地を這ってここに居たい

何が言いたいかというとこれで、プロとして顧客に価値を提供し続けられる集団で価値を発揮し続けられるように頑張っていきたいなと思っています。 この辺りは文字だけでは伝えきれないので、もっと聞きたい方はご飯誘ってください。

お知らせ

今月末2/29(土)にオンサイトのコンテストを開催します。今回は過去2回と比べて規模が大きくなっているので、よりオンサイトの楽しさが味わえるといいなと思っています。 現段階で83/55人の応募を頂いていますが、抽選なのでまだまだチャンスはあります。興味のある方はチャンスと思ってご応募ください。

forcia.connpass.com

作問時のCI環境をTravis CIからGitHub Actionsに移行する方法

競技プログラミングのコンテストを作る際に、作問補助ツールであるRimeを使います。これまではPush時にTravis CIでテストが回るようにしていたのですが、GitHub Actionsに移行してみました。設定を共有します。

Travis CIの例

かつてはTravis CIを使っていました。.travis.ymlの設定を下記に示します。

language: python
cache: bundler
python:
- "3.7"
addons:
  apt:
    sources:
      - pypy
    packages:
      - mono-devel
      - pypy3
install:
- pip install -r requirements.txt
script:
- rime test

GitHub Actionsの例

.github/workflows/rime.ymlに書き換えると下記のようになります。

name: Rime

on: [push]

jobs:
  rime_test:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v1
    - name: Set up Python 3.7
      uses: actions/setup-python@v1
      with:
        python-version: 3.7
    - name: Cache requirements
      uses: actions/cache@v1
      with:
        path: ~/.cache/pip
        key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
        restore-keys: |
          ${{ runner.os }}-pip-
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install -r requirements.txt
    - name: Test with Rime
      run: |
        rime test

Travis CIと比べると、各属性が明確になったように感じます。設定内容は下記で説明されています。

GitHub Actions のコンテキストおよび式の構文 - GitHub ヘルプ

比較

  • GitHub Actionsの良い点
  • Travis CIの良い点
    • Travis CIの方が処理速度が高速です。
      • 何による差なのかは確認していませんが、Travis CIで6m程度かかっていた処理が、GitHub Actionsでは6.5m程度かかるようになっています。

関連記事

Rime関連で他にも記事を書いているので、必要に応じて見てみてください。

matsu7874.hatenablog.com

matsu7874.hatenablog.com