2019-06-27

[] ICFP Programming Contest 2019 02:13 はてなブックマーク -  ICFP Programming Contest 2019 - TopCoderの学習のお時間


https://icfpcontest2019.github.io/

https://github.com/tomerun/icfpc2019

一人で参加した。一人でフル参加したのは何気に初めて。

  • 2009: 途中で萎えた(同時に開催されてたマラソンマッチに移行した)
  • 2010: 休み取ってなくて2日しか参加できず
  • 2011: 問題読んで本格的な関数型の知識要求されてそうで怖じ気づいた(同時に開催されてたマラソンマッチに移行した)
  • 2012-2018: チームで参加

1日目


会社で問題を読んで、帰宅して環境整備から取りかかった。

最低限の目標として、コンテストの順位は二の次で、快適に解答を実行できる環境を整備することに置いていたので、ライトニングは無視です。

AWS Batchで並列実行できるようにすることをもくろむ。そのためにDockerイメージを作る。

Crystalで書くつもりだったのでCrystal公式イメージをベースに作った。

実際の動作はソースコードに同梱したシェルスクリプトを実行して行わせるので、Dockerでやることはs3からソースコードとテストデータを落としてきて、ソースコード解凍して中にあるスクリプトをたたくだけ。パラメタ的なのは環境変数で渡す。

f:id:tomerun:20190627020653p:image

なのでイメージに必要なのはawscli(とそのためのPython)くらい。

けど入れてみたらイメージサイズが700MBくらいになってECRへのアップロードがメッチャ時間掛かっていたので一旦停止して、Alpine Linuxのイメージから自分で作ることにした。

だがCrystalがすんなり動かず、依存パッケージを自分で色々入れないといけないらしい。

https://github.com/sam0x17/crystal-alpine/blob/master/Dockerfile

これを参考にしたけど、最新より一つ古いバージョンの0.28.0で試したものっぽいし、結局これらインストールしてたらイメージサイズも別に小さくなってないしで、結局最初の方針通りcrystal公式イメージから作った。だいぶ時間ロス…

あと結果を入れるためのRDSも立ち上げて、テーブル定義しておく。

ややこしいエンティティ間の関連があるわけじゃないし保守性の制約も緩いから、RDSじゃなくてDynamoDBのほうが適切そうな気もするけど、NoSQL本格的に使ったことないのでとりあえずRDBで。

2日目


イメージの準備ができたのでAWS Batchでジョブを定義していく。

初見では概念が色々ややこしいけど、クラスメソッドさんのブログを熟読してとりあえず動くレベルで設定をした。

昼過ぎぐらいにはジョブを投げて並列で実行されることを確認できた。初回はEC2インスタンス立ち上げからなのでジョブの実行が始まるまで数分かかることを知った。

入力パーサを書き始める。ソルバで使うことを考えて情報を色々リッチに持たせていたらだいぶ時間かかってしまった。

テストケースを画像化しようかと思ったけど、Crystalに良い感じの画像ライブラリがなさそうだったのであきらめた。

このあたりで開始から24時間が経過して、仕様が出そろった。けど最後に出てきた追加仕様は弱小一人チームでは手が足りなすぎて無視するしかないやつで、はい…という気持ちになった。

ライトニングの順位表凍結が解除されたので、とりあえず順位表情報を10分おきに取得しておく。これはLambdaを使って定期実行した。コンテスト中には何も使わなかったけど、グラフ化でもしてみるつもり。

ひとまず正の得点を出すべく、単純に直近のまだ塗っていないところを塗りに行くグリーディーソルバを書いていく。

3日目


最初は単純なソルバからと思っていたのと相反して、Extension of the Manipulator使ったり、直近3歩だけは全探索して一番塗れるパターンを探したり、などいろいろやり始めてしまい、動かせるものになったのはこの日の21時くらいだった。

これで実行環境に投げて実行されている間に、DBからベストな結果を取ってきて提出用のzipにするスクリプトを書き、出てきた結果を提出してみた。

するとちゃんと点数が出て、しかも想像していたよりだいぶスコアが高く、一人で盛り上がった。

テンション上がってきて、Fast Wheels使うのとか、ブースターが近くにあったら優先的に取りに行くのとか、小さな非連結成分を塗り残していたら優先的に塗りに行くのとか、5時近くまで実装やってしまった。

Extension of the ManipulatorやFast Wheelsは取ったら即使うように決め打ち。

Manipulatorの追加位置は、本体から遠い箇所が選ばれやすいようなランダムで。

4日目


引き続きテンション上がっているので9時過ぎぐらいから活動開始。

ビジュアライザで見ると明らかに塗り残しがあるまま先に進んじゃっているのが見られるので、これをなんとかしたいと思う。

直近のまだ塗っていない所を探すときに、現在位置からの距離が直近な場所ではなく、ボットに「ベース位置」みたいなものを決めて、そこからの距離が近い場所を塗りに行くようにした。

あまりにも遠いところになってしまう場合は、現在位置をベース位置としておき直す。

本当はもっと大局的に、盤面をざっくりとグラフ構造としてみてTSPで経路最適化、とかをやりたかったが、シンプルに実装する道筋が見えず、アドホックな改善を積み重ねる形になってしまった(初日の環境整備に使った時間がなければ…)。

Fast Wheelを使ったときの探索がバグりまくって苦しかった。スピードアップしてると細い道から脱出できずに行動できないことがあるとか、最初想像できていなかった…。

自前チェッカのようなものは準備していないので、公式のビジュアライザでちまちま動かすのつらい。

残り5時間くらいで、まだDrill,Teleport,Cloneを使えていない状況で、それらを実装するか、ブースターは捨てて探索を賢くするのを頑張るか迫られた。

が、せっかくある仕様はできるだけ実装しておきたく、ブースターを実装するほうを選択した。

まずDrill。これは取ったらすぐ使うわけではなく、近い位置に塗り残しがなくて次にどこを塗るかBFSするタイミングで、Drillを使ったとした場合の探索も行い、時間が短くなるようなら使う、とした。

Drillを使うことにどれくらい効果があったかはよくわからないが、プラスの意味はあったと思う。

そしてClone。これはLambda Coinガン無視チームにとっては80ケースしか影響しないのであまり気は進まなかったが、やらないとその80ケースが壊滅的になるのは明らかなので、仕方なく実装した。

本当は分裂したBotたちを上手く協調させることをやらないといけないんだろうけど、各Bot独立にこれまでのアルゴリズムで動かしただけ。

それでも目に見えて結果は良くなったので、さすがにやって良かったという気になった。まあトップのスコアから見たら、ひどいゴミが多少マシなゴミになったくらいの変化ですが…。

Teleportは上手く使えそうに思えなく、時間もなかったので無視することになった。

終了1時間前に、最後の実行としてジョブを投げまくっていたら、インスタンスがkillされることがちょこちょこあってちょっと焦った。スポットインスタンスでやっていたので、リザーブインスタンスの環境も急遽立ち上げ。

それでも一部ジョブの実行がなかなか始まらなくてやきもきさせられた。

これ、実行開始のレイテンシが気になるときは、Minimum vCPUを大きめの値にして常時インスタンス立ち上がっている状態にしておくものですね…(学び)。

手元で十分なバリデーションができてなくて、提出したらFailedになるのが怖いので、実行途中で何度か提出して様子を見る。無事最後まで全部validな解になっていて良かった。

感想


ちゃんと環境作れたのは大変良かった。手元でスクリプト一つ流したら勝手に実行して結果を保存してくれるのは最高だ。

これ、マラソンマッチとか他のコンテストでも役立ちそうなので使えるように整備しておこう。

f:id:tomerun:20190627020656p:image

今回は画面は何も作らなかったので、次回はダッシュボード画面を頑張りたい。まあ一人チームでは自己満足にしかならなそうだけど、楽しいのが重要。

ソルバーのほうは、仕様を実装するので精一杯という感じで、全く詰められた感じはしないなあ。

探索ロジックはだいぶアドホックになりすぎたきらいはあって、最初からもっと大局的に考えられれば良かったのかなあという感じ。未実装仕様が残っていることによる実装プレッシャーがあると、じっくり考えるの難しいが…。

f:id:tomerun:20190627020702p:image

クラウド費用は1800円くらい。ソルバ実行をガンガン動かしたのは最後だけだから、こんなもんか。

全部東京リージョンでやってたけど、計算サーバーは別に東京である必要なかった。リージョン変えたら多少節約はできる。

2018-12-11

[]プログラミングコンテストに参加し始めて10年が経ちました 03:02 はてなブックマーク - プログラミングコンテストに参加し始めて10年が経ちました - TopCoderの学習のお時間


これは Competitive Programming (1) Advent Calendar 2018 10日目の記事です。

僕が初めて参加したプログラミングコンテストGoogle Code Jam 2008 なので、今年でプロコンに参加し始めて10年です。

これまでにあったことを振り返ってみます。

-13年目


小学校にパソコンが導入されて、初めてコンピュータを触った。といっても置いてあるだけで何ができるものなのかさっぱり不明で、昼休みのゲーム機としてしか使われてなかった。

-10年目


誰が買ってきたのか忘れたけど家にパズラー(昔とても人気があったパズル雑誌)が1冊あって、ハマっていた。一度解いた問題を消しゴムで消してもう一回解いたりしてた。

中学校か市かの図書館で、数学オリンピックの本を見かけて興味を持つ。内容は全然わからない。

-7年目


大学受験にむけてだいぶ数学を勉強したので、ようやく数学オリンピックに出てもよさそうな気がして参加した。

センター試験そっちのけで過去問10年分くらいやったおかげで、予選はボーダーちょうどで通過できた。けど本戦はさっぱり。「こういうの、完全にそれ用の訓練をしとかないと無理でしょ」と感じた。

関連して、灘や筑駒の数オリ金天才中高生!!! みたいな記事を見て、ほへーそういう世界もあるんだなあ、と思うなど。

-4年目


大学のオープンキャンパスでスタッフしてたとき、資料として置いてあった情報学科の後期入試問題が目にとまった。こんなの。パズル/競プロっぽさありますよね。

高校生の時にこれを知ってたら情報学科に興味を持っていたかもなぁ、と思った。

バイト先にExcelで組まれたシステム(マクロも使っていないのでシステムというほどのものでもないが)があって、触れる人があんまりいなかったのでExcelを覚えてちょこちょこ改造したりしてた。

大学では化学専攻だったものの、この先それを続ける気もあんまりなく、だったら経済的に余裕もないし大学院行かずにさっさと就職してしまおう、と考える。

プログラミングとかほとんどやってないけど論理パズルみたいなの好きだし、まあこれから勉強すれば普通の人よりはうまくやれるんではないか、ということでソフトウェアエンジニアを目指す。

一応、情報処理演習の授業で、Fortranを使って簡単な物理シミュレーションを書くというのはやったくらい。

運良く、未経験歓迎地頭学歴採用みたいな感じ(? 実際のところは知らない)のところに引っかかって就職できた。

-3年目


就職に備えての準備として、学生のうちに基本情報技術者を取るくらいはしておいた。

-2年目


就職。

自然言語処理系の部署を希望してたけど何も経験・業績ない人がそんな仕事できるわけもなく、アプリケーションエンジニアをやる。

とにかく一々わからないことだらけで基本情報技術者取ったくらいじゃあ全然足しにはなりませんなあ、という気持ちになった。

コンピュータ扱う人たちの職場なので行動様式もいろいろ厳密だったりするんだろうか、と思っていたけど、そんなことはなくて、ちゃんと定義されていない言葉が使われたり(例:「ビルド」というのはコンパイルするだけのことですか、jarにまとめるまでのことですか、デプロイするところまでですか)でそんなに特別なことはないんだ、という感想だった。

あと、コンピューターサイエンスっぽい数式を触って…みたいなのはほとんどなく、複数人開発だから結局ボトルネックは人間系、みたいな感じで少々イメージとのギャップはあった。

まあ、地道に黙々実装するのはそれはそれで楽しいのですが。

-1年目


Project Eulerを発見して、埋めていってた。憧れていたけど実現できなかった、コンピュータと数式を使って問題を解く(人工的な問題ではあるけれども)、という営みが楽しくてハマる。

My Job Went To India を読んで、TopCoderのことが触れられていた。この名前を見たのはこのときが最初だと思う。

あと 日本語による最初のTopCoderの紹介として有名なITMediaの例の記事 もたぶん見てたんじゃないかな。

1年目 (0年目は存在しない数え方) : 2008年


Google Code Jamの紹介記事 を見て、何これ面白そう!と思って参加した。

結果は、Round1は通過できたけどRound2でまったくわからなくてTシャツ取れず。

このときは「予選で 500 位以内の方は、最寄りの Google オフィスで行われる準決勝にご招待します」だったんですよね…。超大盤振る舞いだ。

数学パズル的なプログラミングは割と自信があったものの、全然太刀打ちできなかったので、これはもうTopCoderで鍛えるしかない! となって、SRMに参加し始めた。

そしたら、世界中の数学・パズル・プログラミング好きな人たちとネトゲっぽく競えるのが楽しくて完全に沼に落ちてしまった。

ちょうどはてな界隈で ハチロク世代 が盛り上がっていたので、(1986年生まれからはちょっと外れるけど)参加して、そこのメンバーでSRMのたびにskypeチャットでワイワイやってた。

Imagin Cupで3位になって話題になっていた chokudaiさんが参加してきたりして、うわぁ有名人が来たーとか思っていた。

2008年の終わりくらいには黄色に定着した感じ(Easyしか解けてないけど)。

ちなみに、このとき自分が書ける言語がJavaだけだったのでJavaで参加してた。それを今までずっと引きずっている

2年目 : 2009年


SRMの過去問で練習するだけでは飽き足りなくなってPOJを始める(C++の練習も兼ねていた)。

たぶんこの頃が一番コンテストにハマっていて、SRMが平日午前中にあるときはそれに合わせて有給休暇を取ったり、コンテスト終了後はotinn.com(competitiveprogramming.info みたいなやつ)にF5連打したりしてた。

リーマンショックの余波?でSRMの回数が減ってしまって悲しかったので、マラソンマッチに初参加した。 http://topcoder.g.hatena.ne.jp/tomerun/20090129/1233244793

2週間あるうちの1回目の週末が問題読むだけで終わってるあたり、まだそんなにハマってなくてとりあえず覗いてみた、感がありますね。

TCO09 Marathonの予選にも参加して、Round1、Round2はまあ上位は無理ゲーだねーという感じだったけど、Round3で何かハマって20位を取れた。 https://topcoder.g.hatena.ne.jp/tomerun/20090408/1239210636

この年は予選が勝ち抜き制でRound3の10位までがFinal進出だったので、自分にもチャンスがあるかも!? という気になった。

ここでもまだ1回目の木曜-日曜までで計7時間しか使っておらず、まだこの時点ではライト層だった…。

TCO予選で覚醒したか、このあと何度か通常のマラソンに参加して1桁順位を連発し、レーティングは2000超へ。SRMは2000手前くらい。

この年が2回目の開催だったUTPCに参加して番狂わせで優勝した。どうも強い人がことごとく調子が悪かったっぽい。 http://topcoder.g.hatena.ne.jp/tomerun/20090608

ICFP Programming Contestにも初参加した。このときは問題があんまり好きじゃなくて同時にマラソンマッチも行われていたので、ちょっとしか触らなかったけど。

この頃からTwitterをよく使うようになり、競プロ勢っぽい人を多数フォローする。それまでWeb上の記事で見るだけだった人からフォローバックをもらって嬉しくなるなど。

3年目 : 2010年


SRMで赤くなった。TopCoderのコンテストは59回目だった。TCO予選でぐぐっとレーティングを上げられたのが効いた感じ。

診断人さんがニコ生でTCO会場から中継をしていて、激アツ展開に見入った。

この年のICFPCはめっちゃ面白かった(車のやつ)

それと、技術力の幅を広げたくてCTFに初めて参加。これも面白い。 http://d.hatena.ne.jp/tomerun/20101129/1291048564

これ以来、CTFにも年1,2回ほど参加するようになった。

せっかくプログラミングコンテストアルゴリズム勉強しているので実用に使えるようなこともやれないかと思い、機械学習をちょっと触り始めた。PRML読書会に参加しに東京へ行ったり。

それに合わせてUTPCの懇親会にも参加した(確かオンサイト会場もあったけど参加は東大関係者限定だった)。プロコン界の多くの有名人の方々と初対面。

4年目 : 2011年


ラソンマッチで本格的にスポンサーコンテストが始まり、機械学習を勉強し始めたこともあって参加してみた。結果は惨敗で、これはこれで別のノウハウがいるね、となった。

スポンサーコンテストが入るようになったせいか、だいぶ通常マラソンの回数が減ってしまって悲しみ。それでもこの年のうちにマラソンも赤到達。20回目のコンテストだった。

短時間コンテストの情熱はだいぶ落ちてきて、過去問を解いたりといったコンテスト向けの練習に時間を使うのはやめることにした。

codeforcesがこの頃から盛り上がってきていたと思うけど、そんなわけで、参加はたまに時間が合えば、という程度で。

KUPCオンサイトに参加しに京都へ行った。ここでもプロコン界の多くの有名人の方々と初対面できて、数年ぶりの京都ということもあり、大変楽しかった。

Google Developer Day の DevQuiz(イベントに参加するための予選)の最終問題がマラソン問題だったので、時間をかけて取り組んで満点を取った。

満点を取った人だけが参加できるハッカソンにも参加したけど、Web周りの技術のことがまったく分からなくて丸一日座るだけをした。

5年目 : 2012年


色々あって転職した。DevQuizの解法を書いたブログを見て声を掛けてきたらしく、実質プロコン転職だ。

東京に引っ越してきたので色々イベントに参加するようになる。SRM勝手オンサイトとかやってた。

TCO Marathonで予選通過してFinalに参加した。 http://topcoder.g.hatena.ne.jp/tomerun/20121231/1356967269

この年は予選が3回あってそれぞれ上位4人がFinal進出、というルールだったけど、なぜかRound1で赤上位の人が3人しか参加してなくて、ギリ赤くらいだった自分が4位にすべり込めた。

Finalは日本からの参加者が多数だったこともあってめっちゃ楽しかった。

komiyaさんから誘われてプロコンを開いた。 https://autumn_fest.contest.atcoder.jp/

問題を作って出題するのは初めてで、コンテスト準備の大変さを実感した。

このコンテスト主催メンバーでIPSCにも初参加。変な問題が多くて、マラソン系を除くと一年で一番楽しみなコンテストだ。

この年はマラソンマッチで高速化コンテストがあってなかなか楽しかったのだけど、またやってくれないかな… https://community.topcoder.com/longcontest/?module=ViewProblemStatement&rd=15038&pm=11776

6年目 : 2013年


やっぱりアルゴリズムっぽい仕事がしたい! となって転職した。

ローレイヤー高速化の仕事を希望してたけど何も経験・業績ない人が(略)、組み込みエンジニアをやる。

この頃から数年間、JAGに細々と問題を提供していた(ICPC参加経験なくてもJAG入会できるんですよ)。関連して、AOJでICPC過去問をぼちぼち埋めたり。

CodeIQでマラソン問題を出題した。 https://togetter.com/li/559726

1位になったmachyさんがかなり工夫したアルゴリズムで解いていて感激した。

ICFPCに初めて合宿形式で参加し、なにやらうまくいって2位!

この年はコンテスト期間中めちゃくちゃ暑くて、そればかりが印象に残っている。

7年目 : 2014年


前の年の年末からKaggle Santa問題に真剣に取り組んで、5位に入った。正直サンタコンペはマラソンマッチとしては問題が微妙なことが多いんだけど、この回はかなり良かった。

機械学習ラソンにもう一度参加してみたらやっぱり惨敗した。難しい…。

8年目 : 2015年


代休が大量に溜まってたので、TCO Marathon予選に全投入して14日間ひきこもりマラソン生活を送ってみた。が通過できず、時間を費やせば良いというものでもないんだなあという学びを得た。

と同時に、全然人と喋らなくてもちょっとTwitterやっておくくらいで(少なくとも主観的には)社会性を維持するのには大丈夫であることがわかった。

CodeChefのlong contestがマッタリやるのにちょうど良いということを発見して、ときどき参加するようになる。

この年の後半くらいから身の回りが大変になって、コンテストはとんとご無沙汰になる。

9年目 : 2016年


AtCoderが国際化・レーティングシステムを開始して、自分の状況的にも変わってきたのでコンテスト参加復帰。

AtCoderの問題は知識不要なことが多くて、訓練をサボっている自分のような人にとっては取り組みやすくて助かる。

色々あって転職した。仕事でアルゴリズムっぽいことはやらなくても良いので、普通にアプリケーションが作れるようになることを目指していこう、プロコンは趣味で参加を続けられる環境を得られればいいや、と方向転換。

DNA解析のマラソンマッチが面白かった。結果はダメだったけど… https://community.topcoder.com/longcontest/?module=ViewProblemStatement&rd=16683&pm=14187

10年目 : 2017年


kenkooooさんが会社でコンテスト開きたい!! と言っていたので問題を作った。

アルゴリズムコンテストにしちゃうと既存の企業コンテストと差別化が難しいので、ショートマラソンを。 https://beta.atcoder.jp/contests/rco-contest-2017-final

ラソン問題って狙って良い問題作るのはかなり難しくて、案をたくさん出してその中から良さそうなのを選ぶ感じで。コンテストで使ったのは4問だけど、案は20個くらい出した。

5年ぶりにTCO Marathonで予選通過してFinalに参加した。 http://topcoder.g.hatena.ne.jp/tomerun/20171029/1509277486

この年から予選がGP30スコアの合計になったことがかなり有利だった。環境が変わって予選にフル参加できたことも大きい。

DDCC2017で初めて企業コン本戦に参加。

AtCoderでスポンサーマラソンコンテストが開催されたので参加。結果は微妙ではあったものの、参加者も多く、アツかった。

企業合同コンテストで初めてオンサイトチーム戦をやった。自分の気質的なところもあるが、他の人がいると集中が大幅に削がれて、これで効率上げるの相当難しいのでは…となった。ICPCに出てる人たちはどう対策してるんだろう。

Crystal言語が自分の中で盛り上がってきてたので、ABCを埋めたりする。

11年目 : 2018年


またTCO MarathonでFinal進出できた。今年はだいぶ運によるものだった感じはあるけど…。

さすがに海外旅行3回目ともなると慣れて余裕が出てきた。これからもまだまだ参加したい。

社会人枠が広いオンサイトコンテストが増えてきて、BitFlyer、SoundHound、HTTFの本戦に参加。ありがたいことです。

会社でのコンテストも引き続き開催。問題の質を保つの大変だぁ。

おわりに


プログラミングコンテストに参加してなかったらどういう暮らしを送っていたのか、想像できないですね。

次の10年も楽しんでやっていきましょう。

2017-12-16

[] 北大日立新概念マラソンでやった高速化色々 23:59 はてなブックマーク -  北大日立新概念マラソンでやった高速化色々 - TopCoderの学習のお時間


Competitive Programming Advent Calendar 2017 16日目の記事です。

北大日立新概念マラソン、1回目も2回目も、Simulated Annealingの良い近傍を適用できた人が上位になったという結果でした。

僕はこの手の良い近傍を見つけるという問題があまり得意でなく、かわりにナイーブな方法を頑張って高速化して対抗しようとしてしまう悪い癖があり、いろいろな高速化をやりました。その内容について、多少一般化した形にしながら書いていきます。

なお別に高速化のプロではないので、CPUパイプラインを埋める…とか先のキャッシュラインをプリフェッチ…とかの話はしません(できません)。アルゴリズムレイヤ中心です。

動的メモリ確保しない(影響度:☆☆☆☆☆)

とにかく動的なメモリ確保は取り除きます。

たとえば、小さな規模のBFSを何回もやるというシチュエーションはよく現れます(今回では、第2回で1ノードを取り除いたとき残った同じ番号のノードが連結かどうかを判定するのに使いました)。

void bfs(int pos) {
    vector<int> que = {pos}
    for (int i = 0; i < que.size(); i++) {
        // queにpush_backしたり
    }
}

毎回vectorを作るのが遅いので、十分なサイズのバッファをはじめに確保しておき、そこを使い回します。

int bfs_buf[2048];
void bfs(int pos) {
    int que_size = 1;
    bfs_buf[0] = pos;
    for (int i = 0; i < que_size; i++) {
        // bfs_buf[que_size++] = next_pos; とかでキューに追加
    }
}

「十分なサイズ」がとても大きくて全テストケースでその量のメモリを確保するのが嫌な場合は、グローバル(または関数スコープのstatic)なvectorにしておいて、毎回clearして使うのも可。

除算・剰余算を取り除く(影響度:☆☆☆☆☆)

除算・剰余算は遅いのでできる限り取り除きます。

その1:焼きなましの受理判定のときに発生する温度での除算は逆数の乗算にする

bool accept(int score_diff) {
    if (score_diff >= 0) return true;
    return rnd.next_double() <= exp(score_diff / temperature);
}

bool accept(int score_diff) {
    if (score_diff >= 0) return true;
    return rnd.next_double() <= exp(score_diff * temperature_inv);
}

その2:N点の中から動かす位置を決めるときは、乱数に対してNでmod取るののかわりに、順番に選んでいくようにする

void simulated_annealing() {
    for (int turn = 0; ;turn++) {
        int selected_pos = rnd.next_int() % N;
        // selected_pos を動かしたりする
    }
}

void simulated_annealing() {
    int selected_pos = 0;
    for (int turn = 0; ;turn++) {
        selected_pos++;
        if (selected_pos == N) selected_pos = 0;
        // selected_pos を動かしたりする
    }
}

または、候補になる位置をvectorに入れておき、要素を重複して入れることでそのvectorのサイズを2のべき乗にする。

選択される位置に偏りはできてしまうし、メモリアクセスの局所性は悪くなるものの、これで速くなって結果が良くなることもある

void simulated_annealing() {
    for (int turn = 0; ;turn++) {
        int selected_pos = cand_pos_list[rnd.next_int() & (cand_pos_list.size() - 1)];
        // selected_pos を動かしたりする
    }
}

スコア差分計算のために現在の値を保持(影響度:☆☆☆☆)

第1回では、ランダムな2点を交換するという近傍を採用しました。

このとき、スコア変化を調べるため「交換後に得られるスコア - 交換前のスコア」を計算する必要があります。

交換前のスコアは何度も同じ値にアクセスするため、事前に計算して保持しておいた方がよいです。

int calc_score_diff(int pos1, int new_val) {
    int ret = 0;
    for (int i = 0; i < 8; ++i) {
        ret -= graph[node[pos1]][node[pos1 + DIFF_POS[i]]];
        ret += graph[new_val][node[pos1 + DIFF_POS[i]]];
    }
    return ret;
}

int calc_score_diff(int pos1, int new_val) {
    int ret = -pos_score[pos1];
    for (int i = 0; i < 8; ++i) {
        ret += graph[new_val][node[pos1 + DIFF_POS[i]]];
    }
    return ret;
}

ビット並列化(影響度:☆☆☆☆)

1bitの情報はできるだけbitset的データ構造で持って、64bit分(またはSIMD使って128bit, 256bit分)まとめて処理できるようにします。

たとえば第2回では、元のグラフGでノード間にエッジがあるかどうかと、現在のキンググラフに埋め込んだ盤面上でノード間にエッジがあるかどうかをどちらも(オレオレ)bitsetで保持して、スコア変化を V/64 回のループで計算できるようにしました。

https://beta.atcoder.jp/contests/hokudai-hitachi2017-2/submissions/1867989 の823行目あたり

メモリサイズを減らす(影響度:☆☆☆)

メモリアクセスは遅いです。

プログラミングにあまり慣れていなかった頃、「exp関数遅いし、細かく離散化してテーブルに入れておけばアクセス1回で値を引けて速くなるんじゃないの」と思ってやってみて、逆に遅くなったという経験をしたことがある人はそこそこいるんじゃないでしょうか(僕はあります)。

できるだけ使っているメモリがキャッシュに乗るよう、でかい配列はデータ型をコンパクトにします。

第1回では、元グラフでの辺の重みを保持する500*500の配列が大きかったので、はじめint32_tで持っていたのをuint8_tにしました。これだけで1割くらい速くなった記憶があります。

ただし何でも詰め込めば良いというわけでもなく、値が最大で15なのをいいことに1要素4bitで詰め込もうとしたら、今度は大幅に遅くなりました。さすがに自前でマスクしてシフトして、という処理があちこちに入るとそのオーバーヘッドの方が重かったようです。

状況によっては、頑張って詰めた方が良い場合もあるとは思います。

座標を1次元に潰す(影響度:☆☆☆)

普通はフィールドのデータは field[x][y] で持つと思いますが、座標を1次元に潰して field[(x << 6) | y] で扱うと速くなりました。

座標をxy別々に扱わずに済んで、いろいろ簡略化されるからだと思います。

おなじみの

int DX[8] = {-1, -1, -1, 0, 0, 1, 1, 1};
int DY[8] = {-1, 0, 1, -1, 1, -1, 0, 1};

int DN[8] = {-65, -64, -63, -1, 1, 63, 64, 65};

になります。

番兵で条件判定を取り除く(影響度:☆☆)

盤面の端をはみ出たかの条件をif文で書く回数を減らせるよう、盤面を64*64分確保して、実際のデータはxyそれぞれ1ずらした位置に置き、外周1周分を番兵として使えるようにしました。

焼きなまし受理判定の枝刈り(影響度:☆☆)

exp関数は遅いので、スコア差分が圧倒的に悪くてほぼ受理されなそうなら、exp関数を呼ばずに棄却します。

bool accept(int score_diff) {
    if (score_diff >= 0) return true;
    double v = score_diff * temperature_inv;
    if (v < -6) return false; // 枝刈り
    return rnd.next_double() <= exp(v);
}

ループアンローリング(影響度:☆☆)

回数が固定されているループで、呼ばれる回数が多い箇所はループ展開します。

for (int i = 0; i < 8; i++) {
    int next_pos = pos + DN[i];
    // next_pos を使って何か
}

int next_pos = pos + DN[0];
// next_pos を使って何か
next_pos = pos + DN[1];
// next_pos を使って何か
(中略)
next_pos = pos + DN[7];
// next_pos を使って何か
}

また、必ず1回以上実行されることがわかっているループはwhileではなくdo-whileにする、とかもこれに含めて良いかと。

やり過ぎると命令メモリが増えることによりアクセス局所性が悪くなって遅くなっちゃいますが。

BFSで来た方向に戻る移動をしない(影響度:☆)

上でも書きましたが、第2回で同じ番号のノードの連結性の確認のためにBFSを使いました。

シンプルに書くと、次のようになります。

for (int i = 0; i < que.size(); i++) {
    int pos = que[i];
    for (int j = 0; j < 8; j++) {
        int next_pos = pos + DN[j];
        if (node[next_pos] == n && !visited[next_pos]) {
            que.push_back(next_pos);
            visited[next_pos] = true;
        }
    }
}

まず、各点で隣接するそれぞれのノードが自分と同じ番号かどうかを、1bitずつ計8bitの値で持っておきます。

すると、BFS内での同じ番号のノードかどうかのチェックが削除できて、隣接位置すべてを見るループも削減されて、次のようになります。

for (int i = 0; i < que.size(); i++) {
    int pos = que[i];
    int bits = surround[pos];
    while (bits) {
        int dir = __builtin_ctz(bits);
        int next_pos = pos + DN[dir];
        if (!visited[next_pos]) {
            que.push_back(next_pos);
            visited[next_pos] = true;
        }
        bits &= bits - 1; // 最下位bitをクリア	
    }
}

さらに、BFSでキューにpushしたときに、自分に戻ってくる側の移動は見なくて良いはずなので、それを教えてあげると、さらに少し速くなります。

for (int i = 0; i < que.size(); i++) {
    int pos = que[i]; // queに入っている値は、上位16bitが探索する方向の候補、下位16bitが位置を表す
    int bits = pos >> 16;
    pos &= 0xFF;
    while (bits) {
        int dir = __builtin_ctz(bits);
        int next_pos = pos + DN[dir];
        if (!visited[next_pos]) {
            int next_bits = surround[next_pos] - (1 << (7 - dir)); // 自分にすぐ戻る移動を抑制
            que.push_back((next_bits << 16) | next_pos);
            visited[next_pos] = true;
        }
        bits &= bits - 1; // 最下位bitをクリア	
    }
}

0〜N-1 の中から異なる2つをランダムに選ぶときのテク(影響度:☆)

ナイーブにやると次のようになります。

int v1 = rnd.next_int() % N;
int v2 = rnd.next_int() % N;
while (v2 == v1) {
    v2 = rnd.next_int() % N;
}

よく知られているテクとして、次のようにすれば乱数呼び出しが確実に2回で済みます。

(ただし分岐予測は効きにくくなるので速くなるかどうかは場合によるかも…)

int v1 = rnd.next_int() % N;
int v2 = rnd.next_int() % (N - 1);
if (v2 >= v1) v2++;

2017-12-02

[] マラソンマッチは役に立つ 18:29 はてなブックマーク -  マラソンマッチは役に立つ - TopCoderの学習のお時間


Competitive Programming Advent Calendar 2017 2日目の記事です。

近年のAdvent Calendar執筆ハードル上昇に対抗するため簡単な記事にします。

皆さんご存じのように競技プログラミングは役に立ちませんが、マラソンマッチやってるとたまに役に立つので役に立ったことを書きます。

人間を意識したコーディング

短時間コンテストでは自分でクラス作ったりすることはそんなに無いかもしれません。一方、マラソンコンテストで実装が重めのやつは1000行超える規模になって、ある程度は関心事の分離ができた構造になっていないと扱いづらく、なかなか死ねます。

また、整理されたコードを見るのは気分的にも良い。何度も同じコードを見ることになるのでこれは重要です。

計算量の感覚

短時間コンテストだと最悪計算量しか気にしませんが、実データで最悪ケースが来ることはそうそう無く「実際はどんなデータなのか?」を計測・分析して、実用上速いデータ構造・アルゴリズムを使うことになります。

プロコン以外のプログラミングでもこういうのが大事なところは多いかと。

ビジュアライザ

解答やテストデータの様子を確認するためにHTML+JavaScriptツールを作ることはよくやります。場合によってはちょっとしたwebアプリ立てることも。

高速化

高速化やったところで最終的なスコアにほとんど意味ない場合も多いですが、わかっていてもついやりたくなっちゃうんだよね〜、ということを身をもって実感できます。

また、いざ高速化やるぞと取り組むときにはプロファイラ使ったりアセンブラ眺めたりして、これはこれで相当実用的な経験になります。

パラメタ最適化

マラソンマッチの終盤には、最適な答えを出すパラメタの探索をやります。これは機械学習のモデル作成でも課題となってくるところであり、ノウハウ知っておくのは良さそうです。

(僕はグリッドサーチしかやったことないですが…)

また、計算の実行にクラウド使うことも多いので「クラウド環境の活用経験あります!」と言えるようにもなりますね。

英語を利用する機会

英語に慣れなければという気持ちはあるものの、でも慣れていないので英語を実用で使う機会が得づらい、というスタートアップ問題がありますが、forumに書き込むのは自由なので貴重な機会になります。

他の書き込みを見ていると「文法無茶苦茶じゃねえか!」みたいな投稿がわりとあって勇気づけられることも。

まとめ

マラソンマッチをやろう!

2017-10-29

[][] TCO17 Onsite 20:44 はてなブックマーク -  TCO17 Onsite - TopCoderの学習のお時間


10月20日(金)


16時の飛行機で成田から出発。空港に着いたのは2時間弱前で、サンドイッチ食べるくらいの余裕はあった

こういう話があるので今後はもっと早く行った方が良さそう http://www.afpbb.com/articles/-/3148112?cx_position=34

12時間かけてワシントンまで行き、そこから乗り継いでバッファローまで。

乗り継ぎの間は1時間40分で、入国審査のカウンターが一つしか開いていなくて、列がなかなか進まず出発時間ぎりぎりだった。空港内をリアルマラソンしてなんとか(5年前と同じ…)

飛行機内では、機内誌を読んだりiPadで本を読んだり。仕事しようかとも思ってたけど、照明落とされるのでPCを開く気になれなかった。

機内誌にスタンディングデスクの広告が2つ載っていた。流行りか…。

現地時間の17時頃にバッファロー到着。Topcoderの手配でホテルまで送ってもらう。mugurelionutさんと一緒だった。

やっぱりホテルの部屋が一人には広すぎで落ち着かない。

ホテル近くのスポーツバーで夕食。サンドイッチやハンバーガーを頼んだところ、やっぱりアメリカンサイズのが出てきてワイルドだった。

10月21日(土)


朝食はホテルのバイキング。食べ物の種類は少ない。飲み物の種類が多く、持ち帰り用のカップがあるのは良い。

この日は夜にWelcome Receptionがあるだけなので1日オフ。chokudaiさんとりんごさんはナイアガラの滝に行っていたが、僕は体を休ませたいのと英語キーボード練習をしたいのとでホテルに残る。

持参したキーボードでAOJの軽い問題を解いたりしてた。

昼食はホテル近くのdomino's pizzaで。焼きたてのピザおいしすぎる…。

19時からWelcome Reception。会場はホテルとは別で、大学?研究所?のキャンパス。そこが今年のTCOのメインスポンサーでもある。ホテルから徒歩15分くらいで、シャトルバスも出ている。

バスがいつ来るかよくわからず、歩いて向かっているグループについて行った。

会場について参加賞をもらう。なんかノベルティ少なめだ。5年前に会った他のコンテスタントの人たちに挨拶する。

Marathon参加者の人たち、すでにデスクで各自準備を進めている。本来はコンテスト開始30分前からなんだけど、やっちゃっていいのかなあ…と思いつつ自分もやる。まあ、やることはそんなに無いのだけど。JDKが入ってなかったのでインストールしたくらい。英語版Windowsは普段使わないのでPATH通すのに手間取ったりしてた。

デスクの環境はこんな感じ。

f:id:tomerun:20171022082643j:image

MilaninさんがMM95の画像を表示していた。

f:id:tomerun:20171021201906j:image

10月22日(日)


朝は会場まで徒歩で行った。空気が気持ちいい(気温は東京と同じくらい)。

9時-19時でコンテスト。昼食として自席で食べられるサンドイッチを用意してもらえるのは大変ありがたい。

最初数分問題が開けなかったりしたけど、まあこういうのはよくあることなので慌てず待つ。

だいぶオーソドックスな問題だった。とりあえず問題を読んで、テスターのコードを読んでカスタマイズ(別プロセスではなく直接自分の解答を呼び出す・マルチスレッド化・出力に情報を追加)して、空解答を作って動かすまでが40分と、なかなか順調にいった。

10時間ではあまり難しいことできないので、基本的には近い所を取りに行くgreedy。それに評価関数の要素をいろいろ加えていく感じだった。ただ最後2時間くらいは、1位とはかなり差があったのに細かいパラメタ調整に終始することしかできず不毛だった。もうちょっと攻めるべきだった。

インタラクティブ形式でありつつ隠しパラメータがほとんど推測可能なので自分でシミュレーション可能という重要な特徴があるのだけど、それに最初気づいていなくてこの特徴を解答に取り入れることができず、残念。

やっぱり慣れないキーボードだとかなりタイピング速度が損なわれるので、次回は自分用のキーボードを持ち込もうと思った。

終了後は会場で夕食を取りつつAlgo Semifinal1を見る。

ホテルに戻ってからはTopcoderスタッフの方に連れられて2日前に行ったのと同じバーで軽く飲み。

10月23日(月)


さすがに前日は疲れたのでゆっくり目(8時半くらい)の起床。

一人で座って朝食を取っていたら、mugurelionutさんとmarek.cyganさんがやってきて、それからPaulJefferysさんも来て会話していた。途中からヨーロッパにおける政治事情の話になってまったくついていけず置物と化していた。

コンテストの問題についてとか、トピックが限定されていてなじみのあるものならまだなんとかコミュニケーション取れるのだけど…。

昼からナイアガラの滝を観に行った。会場からシャトルバスが出ており、25分ほど。

時間そんなにない&お金それなりにかかるということもあって、滝の下までは降りずに上を歩いて回っただけだけど、カナダ側は一帯が水しぶきで覆われていて、雨が降ってないのにずぶ濡れになる感じで満喫した。

f:id:tomerun:20171023145050j:image

帰りのバスの時間がわからず2時間待つ羽目になり、AlgoとMarathonの人は参加してね!と言われていたData Science Workshopには参加できなかった。

会場に戻り、UI PrototypeとDevelopmentのコンテスト風景を眺めた。みんなハッカソンガチ勢っぽい。使われているエディタが、Atom,VSCode,Sublimeなど様々に分かれていて、Developmentでは使われている言語も様々で面白かった。

10月24日(火)


翌日の飛行機が早いので、この日は早め(6時)に起床。

朝食後に会場へ。UI Design部門のコンテスト風景を眺めた。

このへんのコンテストの種類について書いておくと、こんな感じ。


あと、TCOブロガーであるgorbunovさんがたたずんでいたのでいろいろ話した。

午後からAlgo Final。りんごさんの応援をしつつ観戦する。

その後閉会式・結果発表。ニューヨーク州副知事が来ており、挨拶がいかにも政治家という話しぶりだった(ちなみに開会式にはバッファロー市長が来ていた)。

移動してバー的な店でclosing reception。

Nickolasさんにマラソン問題を作るときに考えていることについて聞いたり。

タコス食べたら相当おなかいっぱいになった。

f:id:tomerun:20171024195228j:image

10月25日(水)


飛行機が朝7時発なのでホテル出発が5時。眠い…

空港で軽い朝食を取って、まずはシカゴまで。

シカゴで4時間待ち。空港をうろついたりサンドイッチ食べたりだらだらしたりしていた。

その後成田まで13時間。本読もうとしたがなんかもう目が乾いてダメだった。半分寝ていた。

10月26日(木)


16時頃に到着。いったん会社に立ち寄って雑務やってから帰宅。

この日はまだ興奮してたのか6時間弱しか寝れなかったけど、次の日に13時間寝てしまってだいぶ疲れてたのだなあと。

また来年参加できるようがんばろう。もっとコミュニケーション取れるように英語も(生きていて英語使う機会がTCOしかないので、これを逃すと一生習得できない)。

2017-10-27

[] Marathon Match 95 CirclesMix 16:10 はてなブックマーク -  Marathon Match 95 CirclesMix - TopCoderの学習のお時間

https://community.topcoder.com/longcontest/?module=ViewStandings&rd=16959

1日目(木曜)


問題を読んだ

2日目(金曜)


テスターをカスタマイズ(いつもの)

  • マルチスレッドでのバッチテスト
  • 結果の出力を良い感じに
  • 問題パラメタを自分で指定できるようにする

3日目(土曜)


テスト画像をwikimediaからクロール

(イラストとか本のページとかの画像は弾くのだけど、基準に主観が入るので迷うのが多かった)

1000枚取ってきたけどそんなに要らなかったかな…

greedyに円を一つずつ置いていくのを実装

  • ランダムな位置・半径で円をたくさん作る
    • すでに生成したものと近すぎる円は生成されないようにする
    • 半径は徐々に小さくする
  • それぞれの円に対して、最適な色にしたときにペナルティがどれだけ減るかを評価
  • ペナルティ減少が一番大きい円を選ぶ
    • 選ばれたやつと重なるもの以外の円は保持しておいて再利用する

また、選んだ円に対して、位置と半径を動かす山登りを実装

4日目(日曜)


高速化色々、SSEも使う

円の最適な色を決めるために、円の内部のピクセルに対して中央値を取る処理が重いので、かわりに円の中心にある7*7の正方形範囲の中央値を使う、という大胆な近似を取り入れたり

最終的に、greedyフェーズでは各ターンで 10000/sqrt(N) 個の円を生成し、選ばれた円に 10000/sqrt(N) イテレーションの山登りを行った。所要時間は6,7秒くらい?

5日目(月曜)


まだまだ高速化

円を置いてしまった後に、時間いっぱい使って繰り返し山登りで改善するのを実装(高速化を頑張っていたのは、これをやる時間を作るため)

山登り対象になる円をランダムに選ぶと、評価のためにその円の上に重なっている円をすべて再描画しないといけなくなって受け入れられないほど遅そうで、そうならないよう1つめの円から順番にひとつずつ山登りした。

ピクセルごとに、未来側に円が何枚重なっているかと色の和を覚えておけば、再描画無しで評価できる。

  • まずN枚目〜2枚目までの情報を計算して、それを使って1枚目の円を山登りする
  • 未来側の情報から2枚目の円の寄与分を取り除いて、2枚目の円を山登りする
  • 以下同様

この問題は、とにかくいかに逐次改善可能な形に持って行くかが勝負だと思ったので、ここが一番の重要ポイントだった

6日目(火曜)


調整色々

前日実装したものが山登りになっていなかった(良い解になっても状態を更新しておらず、初期状態の近傍しか調べてなかった)という致命的なバグを修正

7日目(水曜)


パラメタ調整やる、が、ほとんど変わらず

AWSで36coreマシン借りて35スレッドでテストを実行したら、めちゃくちゃ遅くなってまともにテストできなかった。16スレッドに減らすと普通だったので、コア数もったいないがそれで。

原因は追ってないけど、テスターで画像を読んでるからだろうか?

やらなかったこ


  • はじめは画像を縮小して処理することで高速化
    • やったけど良くならなかった
  • 画像から円をパターン検出
    • seed=1を見るとそういうことを考えてしまいたくなるが、実装も実行もコスト高過ぎそうでやめた

結果


1位とだいぶ差があった。

最終日にしか提出しなかったのは、戦略のつもりではなくて、単に何度も提出するのがめんどいからです

2017-07-02

[][] TCO17 Marathon Round2 20:55 はてなブックマーク -  TCO17 Marathon Round2 - TopCoderの学習のお時間

https://community.topcoder.com/longcontest/?&sc=5&sd=desc&module=ViewStandings&rd=16928

準備フェーズ


とりあえず、感覚をつかむために次の2つの自明解を作った。

  • 毎ターン一番近い敵に攻撃
  • 990人まで貯まったら一番近い敵に攻撃

これだけでもそこそこ勝てている。前者が強い場合と後者が強い場合がだいぶはっきり分かれていて、勝ちやすいケースは前者が強くて、最終的に負けてしまうケースでは後者の方が強い。

これらをうまく組み合わせていく感じかなー、と考える。

スコアが順位ベースで決まるので、何もしない解に負けてしまうと大ダメージだからそれだけは無いようにしないといけない。

敵の方がPowerが高いことから、基本的には不利なんだけど、この不利を緩和したい。端数がたくさん切り捨てられるような攻撃をするといい、とかそういうのがありそう

…と考えていたら、6人以下で攻撃すると1:1でロスなし対消滅できて最もおトクであることに気づいた。速攻で連打するとそこそこ勝てるのは最初実装したやつで分かっているので、簡単に勝てるケースはこれをどこまで最適化できるかのゲームになるんだと理解する。

…で、そこではそんなに差がつかなさそうだからやっぱり難しいケースをどう賢くするかだなあ、という雰囲気になる。

モンテカルロシミュレーションやったりするのかなあと思いつつ、何やるにしても敵のパラメータ(power・attackT・locality)やTroopの行き先推定は必要なのでそいつらを実装していく。

実装フェーズ


そして実装していたら1週間経ってしまったので絶望する https://twitter.com/tomerun/status/864889052826288128

ついで、速攻ケースのシミュレーション+倒すbaseの順番での山登りを書き上げて、ある程度の高速化・パラメタ調整をやる。

この時点でまだ本丸(勝ちにくいケース)に手がついていないのに、もう2回目の週末が終わっていて絶望する https://twitter.com/tomerun/status/866298649680027648

とにかく、速攻で勝てないケースの戦略を作っていく。

とりあえず、全滅させられてしまう直前の逃避戦略を実装する。これはまあやるだけ

最序盤ですぐ倒せるところだけは速攻で倒しておくとして、基本は980まで貯めてから攻撃する。攻撃先をどこにするのかが考えるところ。

単純に一番近いところを攻撃するのは、すでに倒せるのが確定しているところに集まりすぎで損、というのはこれまでの結果から分かっている。

すでに倒せることが確定したbaseへは攻撃せず、他を狙うようにするとそれなりに良くなった。

あと、980貯まっていても、敵の攻撃が向かってきていて、攻撃で人数が半分になった状態では全滅させられてしまう場合は、攻撃せず待つようにしたり。

ここまででもう終了前日になってしまったので、全然細かい調整できてないんだけど観念して提出したら93万点とか出てびっくりした。

最後に、他の敵から攻撃されづらい位置にあるbaseを優先的に攻撃する、というのを入れようとしてみるが、良くならなくて終了

ビジュアライザ

f:id:tomerun:20170702204626p:image

カスタマイズした点は以下の通り。

  • 早送り・巻き戻し可能に
  • growRateに応じてbaseの形を変える
  • baseのindexも表示できるように(デバッグ用、普段はoff)
  • playerごとの合計人数を表示

そのほか


  • 今回は実装が難しそうで、そこまで高速化勝負にはならなさそうだったのでJavaを使った。結果的に、やっぱり実装がボトルネックだったのでこれは正解だった
  • 仲間同士で人数を1箇所に集中させて成長速度を増やすのはちょっとやってみようとしたが、あまり効果が感じられなくてやめた
  • 敵のlocalityも推定した(離散化してベイズ推定)けど結局使わなかった

結果