鉄則本マラソン Mayor's Challenge 感想

E8さんが出版した『競技プログラミングの鉄則』の中に収録されているMayor's Challengeというマラソンの問題をやってみました。 あまり包括的な解説にはなっていなくて、twitterに書きたかったことを書き殴ったような感じです(twitterではネタバレになってしまうかと思って...)。

ネタバレだらけなので問題を解いていない人は注意してください。

book.mynavi.jp

記念撮影: seed=0(0.98M点)

  • 参考文献など
    • terryさんの解説とtwitter
    • tomerunさんのブログ
    • takumiさんの解説
    • 公式解説
  • 解法の概要
    • 初期解の生成
    • 近傍
    • 評価関数
    • optuna
  • 感想
  • 疑問
続きを読む

AHC014参加記録

こんにちは。競技プログラミングのコンテストに参加しているyunixです。 estie プログラミングコンテスト2022(https://atcoder.jp/contests/ahc014)に参加しました。2週間は長くて超大変でしたね。 コンテスト中に考えたこととかやったことを書いていこうと思います。

  • 解法概要
  • 問題概要
  • 最初に問題を見て思ったこと
  • 問題に対するアプローチ
    • 方針を決める際に考えたこと
    • 最終的な解法
  • 実装面での工夫
続きを読む

AHC012参加記録

こんにちは。競技プログラミングのコンテストに参加しているyunixです。 最近行われたAHC012に参加して33位でした。 コンテスト中にやったこと、考えたこと、やればよかったことなどを書いていきたいと思います。

この記事の内容

  • AHC012の概要
  • 解法
  • 学びを得たこと
続きを読む

AWS上にマラソンマッチ用のジャッジ環境を作った

  • 背景
  • システムの概要
  • CDKやその他のコード
  • テスト実行結果
    • コスト
    • Lambdaのメモリに関する実験など
      • LambdaのメモリとCPUに関する仕様
      • Lambdaに割り当てるメモリ量を変えながら実験
    • 不満ポイント

※この記事は包括的な解説というよりは、同じようなことをやろうとした人へのインプットになればいいかなと思っています。C++ソースコード用に書きましたが、少し手を入れれば他の言語でも使えると思います。 AWSを触ったことがない人向けには書いていないです。すいません...

<7/23追記> Lambdaのメモリと処理能力について理解があやふやだったので検証した記録を残しました。メモリは1.8GBくらいにするのが良さそうです。

<8/20追記> 実際にコンテストで使ってみたところ、この構成だと不満が多かったです。それに関するレポートを書きました。

<11/7追記> 実際にプログラムを走らせるLambda(図の2番目のLambda)でプログラムをビルドしていたんですが、毎回ビルドするのは無駄が多いことに気がつきました(最適化オプション付きだとそこそこ時間がかかる)。1番目のLambadでビルドしてバイナリをS3にアップロード&2番目のLambdaでS3からバイナリをダウンロードするとコスト面で少し安くなると思います。 また、最近はStepFunctionsいらないかなという気分になっています。StepFunctionsではLambdaの並列実行を担保していてコードはすっきりするのですが、コストが微妙にかかるのと、処理時間がかかってしまうので、ちょっと微妙かなと思い始めました。並列実行が全部終わることを担保するのはクライアント側の方でやればいいかなと思っています(もしくは全部終わったかを確認するLambdaを作るか)。

<11/13追記> tsutajさんに問題を見つけていただいたのですが、GLIBCのバージョンが古いことでエラーが起きているようです。 コンテナのOSのバージョンを上げるように変更をしました。 月報 (2022 年 10 月) - hogecoder

続きを読む

AHC011でやったことの解説

この記事を書いた背景

こんにちは。競技プログラミングのコンテストに参加しているyunix (https://atcoder.jp/users/yunix )です。 最近行われたAtCoderAHC011に参加して33位でした。 マラソン初心者なりに色々やってみて学びが多かったので、それについて備忘録として記事を書きました。

また、長期のマラソンマッチに時間をかけて参加するのは初めてだったのですが(と思ったけれどHTTFは割とちゃんとやってたな...)、コンテスト中に色々な方法を試して挙動がどう変わるかを調べたり、処理速度を改善したり試行錯誤することが本当に面白かったです。 試行錯誤してプログラムを書くことが面白かったということを伝えたい、AHCのようなマラソンマッチのコンテストに興味をもつ人が増えてほしいという気持ちで記事を書きました。

この記事の内容

  • AHC011の概要
  • 解法の解説
  • 学びを得たこと
続きを読む

CADDi 2018 D Harlequinの解説

CADDi 2018 D Harlequinの解説

D - Harlequin

問題概要

N色のリンゴがあり、各色のリンゴはそれぞれa_i個ある。
先手と後手が以下の行動を交互に行うようなゲームを考える。

  • 一個以上のリンゴを選んで食べる。ただし、同じ色のリンゴを2個以上食べてはいけない。

このとき、最後のリンゴを食べたほうが勝ちになるとする。
先手と後手が双方最善を尽くした時、どちらが勝つか?

考え方の方針

この手のゲームの問題は、"先手必勝の局面"の特徴は何か、ということを考えるとうまく行くことがあります。
もっと簡単な例でいうと、子供のころにやった遊びで、

  • 先手と後手が交互に数字を1から昇順に数える
  • それぞれの手番では1つ以上3つ以下まで数を数えることができる
  • 最後に30を言ったほうが勝ち

というものがあると思います。

このゲームは

  • 30を言ったほうが勝ち
  • 26を言える状況ならば26を言った後に相手に手番を譲る。そうすると相手は27~29のいずれかまでしか数えることができず、どの場合でも自分のターンで30を言うことができる。したがって26を言えたら勝ち。
  • 同様の理屈で22を言ったら勝ち
  • 同様の理屈で2を言ったら勝ち

というわけで、正しくやれば先手が必ず勝ちます。
最初のターンで2までをカウントして、次に相手が3~5のどの数値までを数えたとしても、次に6を言って手番を譲り... ということを繰り返せば最終的に30を言うことができます。

この手のゲーム(いわゆる石取りゲーム)では、"相手が何を言っても最終的には勝てる"というような状態(必勝状態*1 )を決して手放さないような手順を考えることが大事になることが多いです。
この30を言うゲームでの必勝状態は"自分の手番で2+4nの数値までを言った"という状態です(n>=0)。
相手がその次にどのような手を指したとしても、自分のターンでまたこの状態に持ってくる、ということを繰り返すと最終的に勝つことになります。

NimやNimKなどで出てくるGrundy数の解析によって問題を解くのもこの考え方の延長で、"最善を尽くせば後手が必ず勝てるような局面"はGrundy数が0になっていて、局面のGrundy数を数えることで先手と後手のどちらが勝つかがわかります*2

というわけで、このゲームでもそのような"先手か後手のどちらかが必ず勝てる状態"がないかを探してみましょう。

どういうときに必勝の局面が現れるか?

まず簡単のため、一色しかリンゴがない場合を考えましょう。
リンゴの数が

  • 1個の時: これは先手が1個食べておしまいなので、先手勝ち
  • 2個の時: 先手は1個しか食べることができず、一個残った状態で後手の手番にわたる。したがって後手勝ち
  • 3個の時: 同じ理屈で先手の勝ち
  • 以下省略

というわけで、リンゴの数が奇数: 先手勝ち、偶数:後手勝ち、という結果になります。

このことから、どうも各色のリンゴの数の偶奇が大事そうな雰囲気がしてきます*3
最後のリンゴを食べたほうが勝ちなので、リンゴの数が偶数個の時に手番を譲られたほうが不利のような気がしてきます。
そこで、このゲームをプレイするときの戦略として、

  • 各色のリンゴについて、リンゴの数が奇数ならばリンゴを食べる、偶数ならば食べない

という方針でやったら勝てそうです(今は全部偶数の場合は考えないことにします)。

この方針で実際にゲームに勝つことができて、

  • 相手に手番がわたるときには必ず各色のリンゴの数はすべて偶数になっています(0も偶数です)。
  • 相手がどのような手を指してきても、相手が食べた色のリンゴを次のターンで食べることで、その次もすべての色のリンゴが偶数の状態で相手に手番を渡すことができます。
  • 最終的に勝てる人は手番で残っている色のリンゴの数が1個だけになっているような人である(つまり奇数)。したがって、偶数の状態で手番が回ってくる人は必ず負けてしまいます。

というわけで、このゲームにおいて、手番が回ってきたときに"すべての色のリンゴの数が偶数"の時は必ず負ける、逆に"どれか一つでもリンゴの数が奇数の色があるとき"には必勝になります。
最初の局面のリンゴの数、すなわち各a_iの偶奇を判定すればよく、コード自体は以下のようにとてもシンプルになります。

提出コード

Submission #25883195 - CADDi 2018

def main():
    N = int(input())
    a = [int(input()) for _ in range(N)]
    odd_count = sum([(val%2) for val in a]) # aの中の奇数の数の個数を数える
    if odd_count==0: # 奇数の数が0の時、すなわちすべて偶数の時には先手負け
        print("second")
    else:
        print("first")
    
if __name__ == "__main__":
    main()

*1:すいません、これは勝手に名前を付けただけです。この記事の外では使われていないので気を付けてください。

*2:ここではGrundy数については解説しません。たくさんの人が良い記事を書いてくれているので、そちらを探してみてください。AtCoderではABCの水色~青くらいの問題でGrundy数の解析が前提になる問題が出ることがあると思います。Grundy数を知っておくと、与えられた問題をどうやってGrundy数を数えるか、という問題に言い換えることができ、解答への糸口を得られることが多いです

*3:ここから先はあんまり論理的な展開ではないので語尾を濁しています。上のように簡単な例を使って考えると結果的に正しい方針を思いつけることが多いと思います。方針を思いつくまではエスパーでも、それが正しいことが説明できればOKだと思います。