HTTF2023予選お疲れ様でした。1,377,367,921,862点で7位 (perf. 2898) です。人生初のオンサイト出場権を得ることができてとても嬉しいです。
というわけで、解説を書きます。
問題概要
一攫千金を狙う高橋君のためにグラフのエンコードとデコードを頑張ってね。
方針
以下のような方針としました。
- グラフ生成パート
- グラフの同型性判定を行い、互いに同型でないグラフを 個列挙する
- ノイズ対策として、大きさ のクリークを作ってそのクリークを1つの大きな頂点と見なす
- グラフ復元パート
- 焼きなましを使用し、元のグラフのクリーク部分に含まれる頂点がそれぞれ同じグループとなるよう復元を試みる
- 各グループを1つの頂点とみなし、同型性判定を行う
- 以上の試行を複数回行い、正しいと思われるグラフを多数決で決定する
HTTFお疲れ様でした!暫定7位かな?
— TERRY (@terry_u16) 2022年11月20日
グラフの同型性判定(6頂点あれば識別可能)をしました。ノイズ対策としてクリークを作り、各クリークを1つの頂点と見なしました。どの頂点がどのクリークに属するかは焼きなましで求めますが、失敗することもあるので複数回焼いて多数決をしました。 #AHC016 #HTTF pic.twitter.com/MKkrHtLGgm
グラフの生成
まず のケースを考えます。*1 頂点がシャッフルされてしまうので、頂点番号が分からない中でできるだけ多くの情報を伝達したいです。頂点を並べ替えて一致する(同型である)グラフを同一と見なすと、頂点数 と同型でないグラフの種類数の関係は以下の表のようになりました。グラフの各頂点の次数列だけでも多くのグラフを識別することができるので、併せて載せています。
頂点数 | 種類数(同型性) | 種類数(次数列) |
---|---|---|
4 | 11 | 11 |
5 | 34 | 31 |
6 | 156 | 102 |
最大でも6頂点あれば十分なようです。手元のPCで求め、ソースコードに埋め込んでしまいましょう。以下のような手順を踏みました。
- グラフのリストを空で初期化する
- bit全探索で 頂点の無向グラフの 辺の有無を全て列挙し、得られた各グラフについて3.~4.を行う
- 1.のリストに含まれる全てのグラフについて、以下の手順で同型かどうか判定する
- 次数列が一致するかどうか確かめる*2
- 各頂点の順列全探索を行い、グラフの構造が一致するかどうか確かめる *3
- いずれのグラフとも同型でなければ、1.のリストに追加する
のケースはこれで良さそうですが、 のケースではノイズが乗るため誤判定してしまいそうです。そこで、大きさ のクリークを作ってそのクリークを1つの大きな頂点と見なし、頂点数 のグラフを作ることにしました。下図のようなイメージです。「辺1本じゃ足りないならたくさん束ねればええやん!」という発想ですね。
大きさ のクリークを1つの頂点と見なすと、クリーク内には 本、クリーク間には 本の辺が張られることになります。 なので、 のとき は最大で 程度まで取ることができます。二項分布を考えると*4、 のケースで反転する辺の数が半分以上となる確率はクリーク内で 、クリーク間で 程度なので、ノイズに対してそれなりにロバストと言って良いでしょう。
グラフの復元
与えられるグラフを復元したいのですが、
- 頂点がシャッフルされる
- ノイズが乗る
という2つの点が障害となります。
一旦2.のことは忘れて、1.の対処を考えます(と言いつつ2.は最後まで忘れたままになるのですが、それでもそれなりによい結果が得られます)。 正しく復元されたグラフでは、下図のように大きさ の各ブロック部分の辺の有無が全て揃っているはずです。
各ブロックにおいて辺の有無ができるだけ揃うように焼きなましを行いました。より具体的には、
- 解表現: 個のグループのそれぞれにどの頂点が含まれるか
- 初期解: 個のグループのそれぞれに頂点を 個ずつランダムに割り振る
- 近傍:近傍は2つのグループ間のランダムな頂点をswapするだけのシンプルなもの
- スコア: を最大化
として焼きなましを行っています。 ここで、 は 行 列目のブロック内の辺の本数、 は定数(ハイパーパラメータ)です。
焼きなましが終わったら、各ブロック内で辺の有無を多数決で決定することで 頂点のグラフを 頂点のグラフに変換し、順列全探索を行うことで元のグラフを推定しました。*5
問題点として、解説放送にあったようにこの焼きなましは初期解依存が大きい(初期解で多数派の方に寄りやすくなってしまう)ので、
- 焼きなましの初期温度を高くする
- 回焼きなましを行い、それぞれの予測値の和を取って決める
ことを行いました。
細かいテクニック
混同行列
グラフによって正解しやすい・間違えやすいといった傾向があるのではと考え、正解を縦軸、実際の解答を横軸にとった混同行列を各 の組について求めました。下に結果の図を貼っているのですが、グラフによってかなりはっきりとした傾向が出ていることが見て取れます。
解説放送ほどしっかりとした考察はできなかったのですが、
- 個のグラフを選ぶとき、正解率の順にソートして良い方から選ぶようにした
- 「グラフ だと推定したときに正解がグラフ である確率」が分かるので、それを考慮して答えを決めた
といったことを行いました。
焼きなましの高速化
焼きなましではグループ間の頂点のswapを近傍として採用しましたが、これは適切に差分計算を行えば で行うことができます。ここではこれをさらに に高速化する話をします。
グループ から頂点 を削除したときに、グループ とグループ の間の辺の数 がいくつ減るかを高速に数えられればよいです。
- 入力で受け取ったグラフ について、頂点 から伸びる辺の集合をbit列 で表現する
- 焼きなましの解として、グループ にどの頂点が含まれているかをbit列 で表現する
- グループ に属する頂点 を削除することで がどの程度減少するか求めるには、 の立っているビットの数を求めればよい
とすることで、 の減少量を で求めることができました。文章だと分かりづらいので、図を貼っておきます。
注意点として、 と はそれぞれ bit必要となるため、今回の問題では64bit整数では足りず、128bit整数が必要になります。*6
これを行うことで 倍速……とまでは行きませんが、3倍程度は高速になりました。最終的にコードテスト上で最大ケースが0.05秒間に20万回ほど回るようになっています。
パラメータチューニング
スコア計算式を見ると分かるようにグラフの大きさ がスコアに大きく関わってくるため、十分な正解率を保ちつつ を極力小さくするようなパラメータチューニングが非常に重要でした。*7
ここで、チューニングするときに最大化したいスコアの求め方として「 を固定したテストケースを 個回して、スコアの平均値をとる」というようなやり方だと乱数によって結果がブレやすいです。
そこで配布testerを少々改造して、 個のクエリを処理したときの正解率 を出力するようにしました。二項分布を考えると、成功率 の試行を 回行い 回成功する確率は であるため、スコアの期待値は と計算できます。このようにすると乱数によるスコアのブレを抑制しやすくなります。
日記
生々しい方の日記はこちら。考察過程等が含まれていますが、長いです。
感想
初日に問題文を読んで、その面白さに衝撃を受けました。こういった最適化問題を見るのは初めてだったので、AHCの奥深さというものを感じさせられたように思います。そして最初全く手の付け方が分からなくても、仮説を立てて実験して考察を行うサイクルを繰り返すことでだんだん良い解が出せるようになっていく面白さを今回強く実感しました。
今回で長期コンテスト1ページ目ランクインが連続5回目となりました。安定感が出てきたのは嬉しいです。今回は分かりやすく焼くような問題ではなかったので、こういう問題でも良い成績を取れると自信が付きますね。
長期コンテストの勝ちパターンは今までずっと先行逃げ切りだったのですが、今回は序盤でハズレ方針を2回ほど引きつつも終盤追い上げることができたのは良かったなと思います。行けると思った解法が全然ダメだと凹んだりするのですが、そこで諦めないのは大事ですね。
HTTFは2年前に私が道を踏み外す……ではなく、ヒューリスティックコンテストの面白さを知るきっかけとなった思い入れのあるコンテストだったので、本戦進出権を得ることができたのはとても嬉しいです。本戦も頑張ります!