ハル研プロコン2019 参加記
最終順位は未確定だけど多分4位になった
ハル研プロコン4位でした
— Lgeu(るぎう) (@lgeuwce) October 15, 2019
高さがカメの数の半分より大きいエサの巡回セールスマン問題をheld-karp下界を使った枝刈りで解いて、それらのエサを食べる順番が守られる制約をつけてchokudaiサーチ、最後にエサ順列からにエサを一つ選んで近い別のところに挿入する焼き鈍しをしました pic.twitter.com/W76TaozPuQ
人生
人生がおしまいになりそうなのでコンテスト以外で競プロやるのやめます…
— Lgeu(るぎう) (@lgeuwce) September 26, 2019
本日、「ハル研究所プログラミングコンテスト2019」特設サイトをオープンしました!
— 株式会社ハル研究所 (@HAL_Laboratory) September 26, 2019
学生とゲーム開発者の真剣勝負となる当プロコン。まずは特設サイトから参加登録をお願いします。
プログラマーを目指す学生のみなさん、ご参加を楽しみにお待ちしています! https://t.co/qJKLXLa4eo
は?人生おしまいじゃん
— Lgeu(るぎう) (@lgeuwce) September 26, 2019
問題
読んで
書いてないけどパッケージを見るとわかること
エサの数{40, 60, 80, 100}・カメの数{6~25}・高さの分布{低, 中, 高}の直積で全240ステージとなっている
エサ・カメの座標は一様ランダムで、重なって生成されることはない
エサの高さは、[1, カメの数]の離散一様乱数を{低: 3, 中: 2, 高: 1}回発生させた最小値で決定される
やったこと
応募履歴見ながら思い出して書いてるのでやったけど改善しなかったことは結構書き漏らしてると思う
序盤
巡回セールスマン問題は一度取り組んだことがあって(Kaggleサンタ)知見を得ていたので1位を取りたいと思った
過去に類似の問題が出題されてたりしないか探したけど見つからなかった
厳密解が得られないかと思って整数計画問題に落としてソルバに投げてみたけど全然だめだった
ランキングを見たらTopCoder MMの赤い人とかいつもランキングで見るような人たちが上位を占めていて怖気づいたので目標を5位(賞金圏)に下げた
ランキング見たら強い人が上位を占めてて絶望した
— Lgeu(るぎう) (@lgeuwce) October 6, 2019
7日
高さがカメの数の半分より大きいエサは同時に取れない(実質的にそれらのエサをすべて食べて回るカメが存在することになる)ので、高いエサをすべて食べる巡回セールスマン問題(TSP)が解けるか試した
これを参考に枝刈り探索を実装して投げると、高いエサのTSPの厳密解が全ステージ合計で46秒くらいで得られた
TSPで得られた経路の合間に低いエサを食べたい気持ちになったが、どう探索して良いのか実装がさっぱりわからずしんどくなった
ハル研プロコンどう考えても実装が辛くて泣いてる
— Lgeu(るぎう) (@lgeuwce) October 7, 2019
8日
そういえばビームサーチとかいうのあったなとやっと思い出した
焼き鈍しは何回か書いたことがあったけどビームサーチは書いたことが無かったので完全に記憶の外だった
高いエサのTSPはとりあえず置いておいて、全てのエサから最大ターン数が最も小さく済むエサを選び続ける貪欲を書いて投げると72,000turns, 1.5000secになった
幅1のビームサーチに書き換えて投げてみると74,388, 2.1484になった
貪欲と同じ結果になるはずなのにローカルでもなんか悪くなってたので不思議に思った
リファクタリング的なことしたらスコア下がった 完
— Lgeu(るぎう) (@lgeuwce) October 8, 2019
この時点でのビームサーチの状態の持ち方はこんな感じ
struct State { bitset<100> food_is_eaten; vector<Point> turtle_positions; vector<int> turtle_turns; vector<int> permutations; // エサを食べた順 double score; }
ビーム幅を60にしても73,886だった
似た状態が多いからビームサーチの効果が薄いのかと思い、食べたエサの集合が同じ状態はunordered_mapで弾くことにして投げると71,384になった
一つ一つのステージを見ると幅1のときよりスコアが悪いステージがあるので流石におかしいと思って原因を探した
原因は、ビームサーチの後、エサの順番から行動を復元するときに、最も良いエサへのカメの割り当てが複数存在することだった
直すの大変だなーと思ったけどカメのインデックスをソートする前に毎回iotaでリセットしたら直ってくれた
修正したビーム幅60のものを投げると66,984, 39.2265になった
https://twitter.com/lgeuwce/status/1181591630890975237?s=20
怪しい方法で直した
— Lgeu(るぎう) (@lgeuwce) October 8, 2019
ようやくスタートラインに立てた pic.twitter.com/I6zULJQk37
ビーム幅を90にしても66,742にしかならなかった
TSPで高いエサとその間にあるエサを回収する経路を決めて、それで回収しきれないものをビームサーチで探索するのを試したけどあまりいい結果にならなかったのでこの方針はすぐに打ち切った
(画像はステージ229、バグが残っている)
9日
ビューアを見ると最後の方に孤立したエサが残ってしまっていたので、ビーム幅を40に減らしてエサが残り20個くらいになってからはビーム幅を300増やす感じにしたら64,787, 39.4375になった
評価関数の改善(これまでカメの最大ターン数のみだったが、新たにエサの(30, 15)からの上下左右それぞれへの最大距離の和を加える)(基準の座標は角も試したけど中央の方が良さそうだった)をしたりすると62,519, 48.6250になった
そういえば斜め方向を試してもいいかも?と思ってたけど結局最後まで試さなかった
(最終ステージ(エサ100・カメ25・分布高)はスコアが悪くなってる…)
10日
色々とコードが大変なことになったので全部書き直す
コードぐっちゃぐちゃになってるから直さないとなんだけど
— Lgeu(るぎう) (@lgeuwce) October 8, 2019
マラソンマッチ強い人って綺麗にコード書くのも上手いのでは
全部書き直したら実行時間が2/3近くまで落ちてくれた
— Lgeu(るぎう) (@lgeuwce) October 11, 2019
11日
評価関数に残ったエサを結ぶ最小全域木の大きさを入れたい気持ちになったので、マンハッタン距離での最小全域木をO(|V|log|V|)でやる方法を調べて実装したけど重すぎてだめだった
食べたエサの集合が同じものを弾くとビームの終端の方でビームが細くなるので最後の方だけunordered_mapを使わないようにする改善をしたりしたら61,687, 51.4296になった
12日
ビームサーチした後、山登り(エサ順列の2箇所をランダムに選んでrotateかswap)するようにして色々調整したら59,546, 59.6484になった
(次の日くらいにreverseも試したりして結局rotateだけが残った、ただ最終提出では確認してないのでもしかしたらreverseも入れたほうが良かったかもしれない)
60000は切ったけど…うーん… pic.twitter.com/VuI4qPlby1
— Lgeu(るぎう) (@lgeuwce) October 12, 2019
考えてみると焼きなましはor-optしかしてなかった訳なので2-optとかを入れても良かったかもしれない…
— Lgeu(るぎう) (@lgeuwce) October 15, 2019
ちょっとずつしかスコアが上がらないのでしんどくなっている
このままちょっとずつ改善してても30位は安泰だと思ったけど目標は5位なので別の方法を並行して考えていた
高いエサのTSPを調整してみたら20秒くらいになった
高いエサのTSPを解いて閉路を得た後、閉路から順列を適当にひとつ取り出して、それらのエサに関してはその順に取る制約をつけてビームサーチをして山登りをしてみたが66,793, 54.9921だった
13日
山登りを焼き鈍しに変えるのを試したけどあまり良い結果にならなかった
ビームサーチの評価関数に残ったエサの高さの総和/2を加えると58,581, 59.6250に改善した
(この時点で評価関数が最終提出と同じになった)
ビームサーチの評価値、亀の最大ターン数 + 残ったエサの高さの総和/2 + (30, 15)から一番{上下左右}のエサとのその方向の差の絶対値 でやったけど改善できるだろうなあとは思った
— Lgeu(るぎう) (@lgeuwce) October 15, 2019
高いエサのTSPをする解法を改善して、閉路から取り出す順列を任意に(具体的には、高いエサを1つ食べるまではエサを自由に選んで良く、高いエサを1つ食べた時点で他の高いエサを閉路の両隣以外すべてロックし、以降高いエサを食べるごとにその隣の高いエサのロックを解除する)したら58,572, 58.0703になった
(最終ステージは改善が大きい)
3割以上解法変えたのにスコアがほとんど変わらない…
— Lgeu(るぎう) (@lgeuwce) October 13, 2019
3割はTSPを解く20秒の意味だった
このあたりでビームサーチの状態の持ち方は最終提出と大体同じになった
struct State { // ビームサーチの状態 bitset<100> food_is_eaten; bitset<100> food_is_available; // 食べてないエサについて、食べていいかどうか Array<char, 25> turtle_positions; Array<int, 25> turtle_turns; Array<char, 100> permutations; // エサを食べた順 bool closed; // 閉路の好きな頂点を使っていいかどうか int score; }
14日
TSPを使わない方の解法のビームサーチをchokudaiサーチに変えると57,574, 58.0781になった
chokudaiサーチに変えると結果が良くなるのはある程度の確信を持っていて(この問題ではビームの終端で離れたエサが残っているか決まってくるので多様性が重要になり、chokudaiさんの記事の通りchokudaiサーチは多様性を確保できるため)(chokudaiサーチで多様性を確保できる理由は正直はっきり理解できていない)、数日前から考えていたが、せっかく一度最後まで探索するなら結果を次の探索にフィードバックできないか考えていたのでなかなか実装を始めていなかった(この可能性はshindanninさんが言及している)
chokudaiサーチ、深さ優先探索のように、いったん最後まで計算するので、最後まで計算したその情報を生かして、評価関数を変えたりできないか考えているのじゃ。前回のMM100でいうと、取れなかったゴミの部分を取った場合の評価を高くして、2周目以降でゴミをとりやすくするとか。
— nico_shindannin (@nico_shindannin) May 2, 2018
一度最後まで探索し、最後に残ってしまったエサにペナルティをつけ、次の探索ではそのエサが残っていたら評価関数に加算するようにすれば、そのエサは早く食べられるようになるはず
ただ、ペナルティを具体的にどう決めるかとか、実装が大変そうとか考えて無限に足踏みしていた
結局、これをすると探索して見つけたすべての状態に対して評価関数を再計算しなければならないと思い断念し(各エサについてのペナルティが二値ならbitsetで所謂64倍高速化はできそうだとは思ったが)、普通のchokudaiサーチを実装した
ただ、コンテスト後のchokudaiさんのツイートを見るとこれをすれば実現できたかもしれないとなった
何を言っているかと言うと
— chokudai(高橋 直大)🌸🍆🍡 (@chokudai) October 15, 2019
・雑な評価でとりあえずランキングを作る
・次の状態を探す前に、厳密な評価(もしくはそれに近い評価)を計算し直す
・厳密な評価が高い順に計算したいので、再度優先度付きキューに入れる
みたいなことをしたい感じ。厳密な評価は実際に遷移を探す状態の3倍しか行われない
chokudaiサーチのイテレーション数を増やすとメモリが足りないっぽくRuntime Errorが出るようになったので、状態をchar型で持つようにしたらなんか実行時間が短くなった
回す回数が増やせたので57,186, 58.1953になった
chokudaiサーチするときにメモリが足りなかったので状態をchar型で持つようにしたんだけどなんか早くなったのでマジか…となったことがあった(int型とのキャストで遅くなると思ってた
— Lgeu(るぎう) (@lgeuwce) October 15, 2019
TSPを高速化した
プロファイラを使ったらソートがボトルネックになっていたので、比較関数の式の計算結果を予め配列に確保しておいたりしたら9秒になった
(他の初期化処理も含めて9秒なので本当はもう少し早いかも)
このときに限らずVisualStudioのプロファイラを使ってこまめに色々と処理時間を改善していた
いつぞやのWCEの活動でもすに習ったプロファイラを使ってみてるんだけど、vectorのpush_backってこんなに遅いの?
— Lgeu(るぎう) (@lgeuwce) October 9, 2019
reserveしたのにvectorのpush_backが遅いと思ったら、vectorをコピーしたときにcapacityはコピーされてないのに気づいてないだけだったということがあったりした
ハル研プロコンを通してVisualStudioに慣れていった
デバッガとプロファイラが便利だった
TSPの制約をつける方のビームサーチもchokudaiサーチに書き換え、57,109, 55.4062になった
高速化とか調整とかをすると56,539, 59.3593になった
15日(最終日)
最終日のBGM
山登りを焼き鈍しにしてスコアが良くならなかったのを思い出すと、山を登りきれていないんじゃないかという考えになる(前日とかに山登りで一定回数スコアが上がらなかったらbreakするみたいなことをしたけどうまく行かないとかもあった)ので、スコアが改善しそうなところに絞ってrotateをしたい気持ちになる
エサ順列から区間を選んでrotateするとき、区間の両端のエサは距離的に近い方がそりゃ改善しやすいはず
距離の近いエサの対を選ぶのにはマンハッタン距離最小全域木の前処理を流用できた
流用しやすくなるのでコードをきれいに書くのはやっぱり大事っぽいと思った
これがうまくいって54,630, 55.1484で目標の5位に到達した
うおうおうおうおうおうおうおうお pic.twitter.com/fAyNDyEIKb
— Lgeu(るぎう) (@lgeuwce) October 15, 2019
エサの挿入先に少しランダム性を出したり(選んだ挿入先が順列のn番目だったとして、ランダムにnに±1する)、山登りを焼き鈍しに変えたりするとさらにスコアが伸びて4位になった
まだ舞える pic.twitter.com/EoOc1yRvIq
— Lgeu(るぎう) (@lgeuwce) October 15, 2019
この後は焼き鈍しパラメータの調整とかをして終了した
思ったこととか
目標を達成できて良かった
- 目標があったおかげで目先の小さい改善に気をとられないで済んだと思う
- 去年はReleaseビルドを知らないで手元の環境おっそ!!!!とか言ってたし成長したなあ…
評価環境へのoverfitがどこまで許されるのかわからなくてしんどい
問題規定には
特定のステージに依存したチューニングを行うことを禁止します。例えば、X番目のステージの時は、あらかじめ用意した配列に基づいて行動を決定するというような処理は禁止とします。
とあるけど、プロコン速報 vol.2 には
パッケージに含まれているチェックプログラムのコードを見るとわかりますが、全てのステージは同じではなく、
・低いところが多いステージ
・高いところが多いステージ
・二つの中間のステージ
の3段階のステージがあります。ステージの特徴を踏まえてカメを動かすことで、効率よくエサが取れるようになるのではないでしょうか。
とあって、ステージを3つに分けてチューニングすることは許されてそうな雰囲気はある気がする
ステージを分けてチューニングしたら結果が良くなるのはそれはそうだけど、あからさまにやるのはまずそうなので、間接的にやる方法を考えたりした(結局あまり上手い方法が見つからずできなかった)
(去年は30位に入るためにステージを特徴から2つに分けて戦略を変えるとかをしたけどかなりアレだなあと思っている)
上位が強すぎる
- 自分が最初の提出をした時点からほぼ不動だったしいい解に行き着くのが早すぎる
- めちゃくちゃ時間つぎ込んでも上位陣の中盤のスコアに勝てないのはきびしい…
最終提出のイテレーション数を10倍にするのを試したらローカル(初期シード)で52,317になった
- 最終ステージのスコアは366だったけど他の戦略で363が出たことがあったので最適解からはまだ遠そう
焼き鈍しを本来の意味(?)で使えたことがないのでこのままで良いのか不安がある
- 十分に高い温度からゆっくり焼き鈍すと最適解に行き着くのが本来の焼き鈍しだってどこかで聞いた気がするけど、現状山登りの亜種みたいにしか使えたことがない
- 本来の焼き鈍しと違うことをしているなら遷移確率の式(p=exp(dE/T))も最適なのか気になる気がする
- まあでも多分温度管理とかの方が重要そうなので気にしてもしょうがない部分な気もする
- 何もわからない
- まあでも多分温度管理とかの方が重要そうなので気にしてもしょうがない部分な気もする
C++って意外とそこ最適化してくれないのかーってことがあったりするなあと思った
k-means++とかでクラスタリングする解法の人が何人かいて、そういうのもあるのかと思った
赤コーダーに褒められて嬉しい
これかなり好きです 賢い
— リッキー@eiyatonari (@rickytheta) October 15, 2019
赤コーダーに褒められた!!!!!うれしい!!!!!!
— Lgeu(るぎう) (@lgeuwce) October 15, 2019
- chokudai幅
そういえばchokudai幅の意味を途中までiteration数と勘違いしてた(chokudai幅についての確かな資料この世に存在してなくないですか?)
— Lgeu(るぎう) (@lgeuwce) October 15, 2019
まとめ
人生…