2022年11月26日土曜日

HTTF 2023 予選 (AHC 016) 3位解法の解説

2022年11月に開催された HACK TO THE FUTURE 2023 予選 (AtCoder Heuristic Contest 016) の3位解法の解説をします。

\( \epsilon = 0 \) の場合

この場合は厳密に解けるので、特別扱いします。頂点数毎の非同型なグラフの個数は OEIS で調べられます(リンク)。これによると、頂点数4では11個、頂点数5では34個、頂点数6では156個のグラフが存在します。よって、必要なグラフの個数 M に合わせて、頂点数 N を選んで、非同型なグラフを M 個構築すれば良いでしょう。

非同型なグラフを列挙したり、クエリに答える際に、グラフの同型判定が必要になりますが、同型なグラフの「標準系」を定義するのが簡便でしょう。グラフの文字列表現は問題文で定義されていますが、文字列表現が辞書順最小になるような頂点の番号付けを標準系とすれば良いです。

解法1: 次数列で分類する

まず、グラフの次数列(各頂点の次数を降順に並べたもの)に注目する解法を考えましょう。これは最終的な解法ではありませんが、部品として利用します。

基本的な考え方は、グラフの次数列が \( (d_1, d_2, \ldots , d_N) \) であるならば、ノイズを与えたあとのグラフの次数列の期待値は \( ( (1 - \epsilon) d_1 + \epsilon (N - 1 - d_1), \ldots, (1 - \epsilon) d_N + \epsilon (N - 1 - d_N) ) \) になるだろうということです。

注:この考え方は実は正しくありません。yunix さんのこちらのツイートを参照ください。ただ、ある程度の近似にはなるので、この記事では「ある程度の近似になっている」として進めます。

とりあえず、できるだけ多様な次数列を持つグラフを M 個構築したとして(方法は後述します)、クエリの答え方を考えます。

構築したグラフそれぞれについて、ノイズを与えたあとのグラフの次数列の期待値を求めておきます。そして、与えらえたグラフの次数列を計算し、ノイズを与えた後の次数列の期待値がそれに最も「近い」グラフを返せば良いです。「近い」の定義ですが、次数を降順にソートした後の次数列をベクトルと見てユークリッド距離を計算すれば良いでしょう。

この方法がうまくいくためには、できるだけ距離が離れている次数列を持つようなグラフを構築する必要があります。私は次のような方法でグラフを構築しました。
  • ランダムなグラフを大量に準備する。
  • まず、空グラフと完全グラフを選択するグラフの集合 S に追加する。
  • 次の操作を S のサイズが M になるまで繰り返す : 準備したランダムなグラフのうち、S に追加されているグラフの次数列との距離の最小値が最大になるようなグラフを S に追加する。
ランダムなグラフの準備の仕方ですが、単純にランダムに辺を選択すると、なだらかな次数列を持つグラフばかりができてしまうので、偏りがある次数列を持つようなグラフをたくさん作ることが必要です。そのためには、様々な大きさのクリークや独立集合を組み合わせたグラフを作ると良いでしょう。行列表示だと以下のようなグラフです。




解法2: 辺の有無の差が最小になるようなグラフを選択する

解法1ではグラフの情報を次数列しか使っていません。次数列が同じグラフはたくさんあるので、これは多くの情報を使っていないことになります。

そこで、与えられたグラフに対して、辺の有無の差が最小になるようなグラフを選択することを考えます。

辺の有無の差を計算するには、ターゲットとなるグラフを固定した上で、頂点の対応付けを求める必要があります。これを簡単にするために、構築するグラフは次のような 3x3 のブロック行列に対応するようなグラフに限定することにします。


そうすると、頂点の対応付けは、各頂点がどのブロックに対応しているかだけを決めれば良いことになります。そのために、次のような山登りアルゴリズムを実装しました。
  • まず、頂点を次数順にソートして、ターゲットとなるグラフの次数順にソートしたブロックに対応させる
  • 違うブロックに対応している任意の二頂点を選んで、入れ替えたときに一致する辺が増えるかどうかを判定し、増えるならば入れ替える。これを繰り返す。
このアルゴリズムを M 個のグラフに対して実行して、辺の有無の差が最小になるようなグラフを選んで出力すれば良いのですが、そうすると山登りに十分な時間を使うことができず、うまくいきません。

そこで、解法1を利用して、次数列だけに注目して候補を10個に絞ってから、上述の山登りアルゴリズムを実行して、辺の有無の差が最小になるグラフを選ぶという方法を実装しました。

このように、精度が低いが高速な方法で候補を絞ってから、精度が高い方法でランク付けをする手法は、実際の検索システムや推薦システムでよく使われている手法で、一般的に有効な考え方と言えるでしょう。

次に、M 個のグラフを構築する方法を考えます。解法1と同じように、集合から一番「遠い」グラフを逐次追加していく貪欲法を行いますが、今度は距離の定義を「辺の有無の差の数」で定義することにします。

ここで上述山登りアルゴリズムを使うと時間がかかりすぎてうまくいきません。そこで、ブロックグラフ同士のみを比較することに注目して、ブロックの並べる順序を \( 3! \times 3! \) 通り試して、辺の有無の差の数が最小値を計算しました(両方のグラフのブロック順序を反転しても変わらないことに注目すると半分にできます)。

その他の工夫

頂点のマッチングが成功しやすいグラフの作り方
上述の山登りアルゴリズムが成功するためには、次数順に頂点を対応させる初期状態がそれなりに良い必要があります。そのため、ブロック毎の次数の差が小さいグラフは採用しないようにしました。具体的には、少なくとも次数の差が N / 10 以上あるグラフのみを採用することにしました。

空グラフ・完全グラフとの混同が起きやすい問題
公式解説でも言及されていますが、空グラフにノイズを与えたグラフに、一部密度が高い部分ができてしまって、他のグラフと混同しやすいという現象があります。完全グラフについても同様です。

この問題に対処するために、構築するグラフ間の距離を計算する際に、片方が空グラフまたは完全グラフになる場合には、距離を0.5倍扱いするというハックを入れました。非常にアドホックなハックですが、スコアには有効で、コンテスト終了2日前に7位から1位に浮上することができました。

3x3 ブロック行列以外のパターン
\( \epsilon \) が小さい場合は 4x4 ブロック行列が有効な場合があったので、採用しました。また、\( \epsilon \) が大きい場合は 2x2 ブロック行列のみを使った方がスコアが高くなったので、そのようにしました。\( \epsilon \) が大きい場合はブロックが3個あると頂点のマッチングに失敗する確率が上がりすぎてしまったからだと思われます。

当たりの乱数ガチャを固定
各 \( M, \epsilon \) に対して最もスコアの期待値が高くなる N は事前計算で求めておいて固定します。これに加えて、スコアの期待値が高くなる乱数も固定します。上述のアルゴリズムでも、距離が同じグラフが複数ある場合にどのグラフを選ぶかで最終的に選ばれる M 個のグラフは変わってきます。そこで、乱数の seed に対して決定的に動作するようにアルゴリズムを実装した上で、各 \( M, \epsilon \) に対して最もスコアの期待値が高くなる seed を事前計算しました。これは細かい改善ですが計算リソースを必要とするため、最後の一押しとして実行しました。

2022年11月1日火曜日

AHC015 プレイアウトを使ったモンテカルロ法による3位解法の解説

2022年10月に開催されたトヨタ自動車プログラミングコンテスト2022 (AtCoder Heuristic Contest 015) の3位解法の解説をします。

まず、シンプルなルールベースによる解法を解説します。これだけで本番100位以内相当のスコアを出すことが可能です。

解法1: 上下に分割する

まず、イチゴ味(タイプ1)のキャンディーを上に集めることを目指します。

このためには、次のキャンディーがイチゴ味の場合は、下に傾けると良いです。すべてのキャンディーが下側に集められ、イチゴ味のキャンディーがその上に配置されるからです。

逆に、次のキャンディーがイチゴ味以外の場合は、上に傾けると良いでしょう。

このルールを実装すると次のような挙動になり、イチゴ味のキャンディーを上に集められていることがわかります。



この解法によって 104M のスコアを出すことができました(コード例)。これは本番286位相当のスコアで、これだけで正の得点を獲得した778人中の上半分に入ることが可能です。

解法2: 上・左下・右下に分割する

よりスコアを高めるためには、イチゴ味のキャンディーだけでなく、スイカ味(タイプ2)とパンプキン味(タイプ3)のキャンディーもできるだけ同じ場所に集めることが必要です。そこで、イチゴ味のキャンディーを上、スイカ味のキャンディーを左下、パンプキン味のキャンディーを右下に、できる限り集めることを考えます。

解法1と同じく、次のキャンディーの種類によって、傾ける方向を決めます。

次のキャンディーがイチゴ味の場合は、解法1と同様に下に傾ければ良いです。

次のキャンディーがスイカ味の場合は注意が必要です。

現在キャンディーが下側に集まっている場合(つまり、最後に下に傾けた後、一度も上に傾けていない場合)は、スイカ味のキャンディーをイチゴ味のキャンディーの上側に配置することを防ぐため、上に傾けましょう。この場合、スイカ味をパンプキン味よりも左側に配置されるようにすることはできませんが、仕方ありません。左右の関係よりも上下の関係を優先することにします。

一方、現在キャンディーが上側に集まっている場合(つまり、最後に上に傾けた後、一度も下に傾けていない場合)は、右側に傾けることで、スイカ味のキャンディーが左下に配置されるようにすることができます。

次のキャンディーがパンプキン味の場合も、スイカ味の場合と同様に、現在キャンディーが下側に集まっているか上側に集まっているかで場合分けしましょう。

このルールを実装すると次のような挙動になり、イチゴ味を上、スイカ味を左下、パンプキン味を右下に、おおまかにですが集められていることがわかります。


この解法によって 133M のスコアを出すことができました(コード例)。これは本番97位相当のスコアで、今回はこのヒューリスティックを発見できれば非常に短いコードで100以内に入ることが可能な回でした。何と盤面のシミュレーションすらしていないのです!

準備: 盤面のシミュレーションとスコア計算

より高度な解法に移る前に、盤面のシミュレーションとスコア計算が必要です。

盤面を傾ける操作は愚直に行ごとまたは列ごとに処理するしかないのですが、最終的にボトルネックになるため、不必要な遅い操作(メモリ確保など)が無いように気を付けましょう。

スコア計算では連結成分の計算が必要になりますが、深さ優先探索または素集合データ構造のどちらでも良いでしょう。

解法3: プレイアウトを使ったモンテカルロ法

では、3位解法となる、プレイアウトを使ったモンテカルロ法による解法を解説します。

ある時点で、次に盤面を傾ける向きを決めるときに、それぞれの向きに傾けたときの最終スコアの「期待値」を計算し、一番期待値が高い向きに傾けることを考えます。もちろん厳密に期待値を計算することはできないので、近似する方法を考えます。

まず、残りのキャンディの個数が N 個ある場合、有り得る残りの入力列は N! 通りあります。もちろんすべてを試すことはできないので、ランダムな入力列をいくつか生成してそれに対するスコアを計算し、平均値を取ることにします。

入力列を固定しただけではスコアを計算することができません。出力列(傾ける方向)も決める必要があります。今の目的は次の出力を決めることなので、次の出力だけを固定し、残りはランダムに選ぶという方針が考えられます。しかし、これではあまりにランダム性が高く、実際のスコアより大幅に悪いスコアになってしまうので、次の出力候補を比較するにはあまり役に立ちません。

そこで、次の出力だけを固定し、残りは解法2のアルゴリズムによって出力を決めることにしましょう。そうすると、高い精度で次の出力を固定したときのスコアの期待値を推定することができるようになります。これが可能なのは「解法2が非常に高速であり、かつ最適解にそれなりに近い」ためです。このように、高速なヒューリスティック(場合によってはランダム)によって出力を決定して最終状態のスコアを計算することを「プレイアウト」と呼びます。プレイアウトは、囲碁 AI などに用いられているモンテカルロ木探索において重要な概念です。興味のある方は調べてみてください。プレイアウトは、途中状態で盤面の良さを評価する評価関数を作るのが難しい場合に有効です。

この解法では、何個のランダム生成列を処理できるかによって性能が変わってきます。また、ゲームの序盤・中盤・終盤でそれぞれどの程度時間を使うかという時間管理も重要でしょう。私は毎回固定で150個のランダム生成列を処理するという方針を実装し、156M のスコアを出して3位を取ることができました。終了後にリファクタリングした後のコード例はこちらになります。

HTTF 2023 予選 (AHC 016) 3位解法の解説

2022年11月に開催された HACK TO THE FUTURE 2023 予選 (AtCoder Heuristic Contest 016) の3位解法の解説をします。 \( \epsilon = 0 \) の場合 この場合は厳密に解けるので、特別扱いします。頂点数毎の非...