TERRYのブログ

最近は競技プログラミングにお熱。

AHC044 解説

AtCoder Heuristic Contest 044お疲れ様でした。149,801,634点で2位です。やったー!

今回も軽めに解説記事を書いていきます。

ビジュアライザ (seed=0, score=998,314点)

問題概要

いい感じに掃除当番を決めてね。

atcoder.jp

解法

概要

  • 焼きなましをしました。
  • 評価はシンプルなシミュレーションです。
  • シミュレーションは L を少しずつ大きくして徐々に精緻化していきました。

解の持ち方

解は a_i, b_i をシンプルに持ちました。

初期解は本当に適当で、  a_i=b_i=i としています。適当すぎ……。まあ焼ける問題なので大丈夫です。

評価関数

上位陣は「親の目標値/2の和が自身の掃除回数になる」というような考察を行っている人が多かったように思いますが、私は愚直にシミュレーションしました。(あの……)

その代わり、シミュレーションの長さ L_kL_k=\lfloor 500000/2^{8-k} \rfloor (k=1, 2, \cdots, 8) と定め、 3906, 7812, \cdots, 250000, 500000 と2冪で伸ばしていきました。これにより徐々に評価の精緻化を行っていきます。L_k で焼きなました解を  L_{k+1} の初期解として、焼きなましを8回行うイメージです。

グラフの連結性を考慮しなくてもよいため、実装はちょっとだけ楽になります。

近傍

近傍は1点変更と2点swapがベースです。c=(a_0, \cdots, a_{N-1}, b_0, \cdots, b_{N-1}) として、

  • 1点変更: i をランダムに1つ選んで、  c_i をランダムな値に変更する
  • 2点swap: (i, j) のペアをランダムに選んで、 c_ic_j をswapする

という近傍です。

この近傍で焼きなましをしてみると、実行時間を伸ばせばスコアが伸びること、近傍の受理率がさほど高くないことが分かります。愚直シミュレーションのせいでスコア計算が重いので、近傍の計算量を少し増やしてでも筋の良い近傍を選ぶことができるとスコアが伸びそうです。

そこで、完全にランダムに近傍を選ぶのではなく、「なんとなくスコアが上がりそうな近傍」を選ぶような工夫を入れていきます。2点swapの場合を例に取ると、以下のような近傍選択を行いました。1点変更の場合も概ね同様です。

  1. 目標値との差異 \Delta T_i=|t_i-T_i| に比例した重みつき確率でランダムに社員を選ぶ
  2. 社員 i に向かう有向辺 e^i と社員 j に向かう有向辺 e^j のペア  (e^i, e^j) を列挙し、社員 i, j だけを見たときの辺のswapによるスコアの改善量 \Delta S_{e^i, e^j} を計算する
  3. \Delta S_{e^i, e^j} が0以下のもの(スコアが悪化することが見込まれるもの)を候補から外す
  4. \Delta S_{e^i, e^j} に比例した重みつき確率でswapする辺のペア (e^i, e^j) をランダムに選ぶ

1.の重みつき確率は「差異の大きい社員を重点的に改善したい」お気持ちを、4.の重みつき確率は「解が大きく改善しそうな近傍を重点的に選びたい」お気持ちをそれぞれ込めています。2.のスコアの変化量は本来社員 i, j の子孫全てに影響が波及するのですが、ここでは「なんとなくスコアが上がりそうな近傍」を選ぶことが目的なので一旦無視しています。*1

ちなみにこの近傍を入れない状態の解は148.001M点程度(133位相当)でした。相当変わりますね。

なお、最終的には重みつき確率を \Delta T_i, \Delta S_{e^i, e^j} から \Delta T_i^4, \Delta S_{e^i, e^j}^4 に変更しています。ほんの少しだけスコアが上がりました。

コンテスト中の立ち回り

ざっくりどのような立ち回りだったか書いていきます。

問題文を読みつつ環境整備(~0h07m)

問題文を読みます。むずい……。

とりあえず環境構築として、公式toolsのダウンロード、pahcerの準備などを行います。

雑焼きなましを実装(~0h23m)

よくわからないので、とりあえず雑な解法を実装して様子を見ることにします。5分くらいで考えた解法を雑に実装して感触を確かめるのはよくやる手段です。

AHC039に少し思いを馳せると、最初は L を粗くして徐々に細かくすることでなんとなくいい感じになりそうな気がするので、そうします。まずは1点変更の焼きなましを実装して投げると139.80M点になりました。まあこんなもんかという印象です。

2点swapを実装(~0h28m)

明らかに2点swapを入れた方が良いので、入れます。147.68Mで暫定3位になり、思ったよりスコアが出せる問題なんだなという気持ちになります。

今回の問題は最終的なスコア目標をどこに置くかで方針が変わってくるので、この情報は結構大きいです。

考察とリファクタ(~1h24m)

さすがに5分くらいで考えた解法では勝てないので、ちゃんとした考察を試みます。

何か賢い構築型解法がないか考えて、10000, 5000, 2500, \cdots と2冪で t_i を作る解法が思い浮かびました。ただ現時点でトップが149M台になっていて、1人あたりの誤差を 100 未満にすることが要求されており、どう頑張っても達成できなさそうなことから棄却しました。

現時点での近傍を見ると、辺の付け替えにより解が大きく変わるので明らかに効率が悪そうです。焼きなましの実行時間を伸ばすとスコアが上がりますし、他に良さそうな方針も思い浮かばないので、近傍を工夫する方向性としました。

また、さすがにコードが雑すぎて拡張性が低いのでリファクタも行っています。

近傍の改善(~1h59m)

良さげな近傍だけを候補にしたいので、swapによるスコアの変化量をざっくり見積もり、重みつきで選ばれるような工夫を入れました。149.39M点で暫定5位になりました。悪くなさそうです。

1点変更も同様に改善を入れると、少し伸びて149.46M点になりました。

温度バグ修正(~2h09m)

筋は悪くなさそうなのですが、ズルズルと10位近辺くらいまで順位が落ちてしまいます。うーんと思いつつコードを眺めていると、焼きなましの温度設定がバグっていてほぼ0付近になっており、ほぼ山登りになっていることが判明しました。あの……。

修正すると149.75M点で暫定1位になりました。よしよし。

パラメータチューニング(~3h38m)

その後ちょっとコードをいじるもほとんどスコアは変わらず、やることがなくなったので、焼きなましの温度・時間・近傍選択確率をoptunaでチューニングすることにしました。

Google Cloudで176vCPUのインスタンスを借りてぶん回します。かかった費用は200円程度でした。これにより149.764M(5位相当)から149.790M(2位相当)にスコアが上がりました。

最後にランダム抽選の重みを\Delta T_i, \Delta S_{e^i, e^j} から \Delta T_i^4, \Delta S_{e^i, e^j}^4 に変更して、149.801M点で2位フィニッシュです。

感想

square1001さん・e869120さんwriterの初AHCということで楽しみにしていたのですが、やはりとても面白い問題でしたね。このように色々なwriterの方が作問をされると問題のバリエーションも増えて、よりAHCが面白くなっていきそうで楽しみです。

問題の本質(親の T_i/2 の総和で t_i を見積もれる)に気付いていなかったり優勝できなかったりで悔しい気持ちはあるのですが、それだけ復習のしがいもありそうです。しかしビームサーチかあ……さすがに自分の実力だと思い付きそうになく、精進が必要ですね。

直近のAHCは5回連続で1ページ目に入れていて調子が良いので、なんとかこれを維持していきたいと思います。頑張ります!

*1:お気持ちとしてはhakomoさんの「少なくとも部分的に改善する近傍」的な気持ちを入れています。