AHC 053で優勝することができました。その解法を延長戦で改善したところ、全ケース満点まで到達できたので、その解法を紹介します。
当初は(詳細は省きますが)闇テクを使っていたのですが、最終的には300ケースに1回程度まで失敗率を下げ、闇テクなしで満点が取れるようになりました。
問題
https://atcoder.jp/contests/ahc053/tasks/ahc053_a
マラソンの問題文として前代未聞の短さじゃないだろうかこれ。subset sumの強化版という雰囲気、一方で使う値は自由に指定できる。
解法
カードを使う枚数を固定
各グループのカードの枚数を8枚と固定してしまいます。$A_i$の値から $\frac{10^{15}}{8}$、$B_i$の値から$10^{15}$を引いて考えることで、この問題は次のように考えることができます。
- $A_i$ として 負の値も書くことができる
- $B_i$ として $[-E, +E] (E = 2 \times 10^{12})$ が与えられる
自分の解法だと圧倒的に見通しが良くなるので採用していますが、この言いかえをした方がいいのかは諸説 / 解法依存だと思います。
2段階で合わせる
おそらくここが一番のキーアイデアです。問題を前半 / 後半に分割し、$\sqrt{E}$ 倍ずつ調整します。
- 前半: 250枚のカードを4枚ずつ配る。$B_i$ をすべて $[- \sqrt{E}, + \sqrt{E}]$ の範囲に収める。*1
- 後半: 250枚のカードを4枚ずつ配る。$B_i$ をすべて $0$ にする。
前半と後半の問題は非常に似ており、実際に使用したアルゴリズムもほぼ同一です。
調整アルゴリズム
全てのグループの値を求める範囲に調整する方法ですが、最終的には非常に単純なアルゴリズムになりました。
初期解としてランダムにカードを4枚ずつ配り、以下の遷移をひたすら繰り返します。
- ランダムにグループを選び、4枚 + 未割当50枚から、合計が求める範囲に含まれる4枚組を探す
- 見つかった場合: 見つかった 4枚組を新しく割り当てる
- 見つからなかった場合
- そのグループがもともと調整済みだった場合: 何もしない
- そのグループがもともと調整済みではなかった場合: 54枚からランダムに4枚選んで割り当てる
山登りのようなアルゴリズムではありますが、調整済みかどうかだけを気にしており、誤差の大きさなどは一切気にしていないことに注意です*2。
54枚から 4 枚組を探す方法
上記のアルゴリズムのボトルネックは「4枚 + 未割当50枚から、合計が求める範囲となる 4枚組を探す」部分です。素直に探すと $C(54, 4) = 316251$ 程度の組合せを見る必要があります。ここで、
- 54枚をランダムに 27枚 * 2に分割し、 「前半から2枚 + 後半から2枚」という選び方のみを探す
と探索範囲を絞ることで、半分全列挙が行え、高速化ができます。かなり定数倍高速化をしたところ、最終的に2秒で 20万回弱 程度この探索が回るようになりました。
細かい工夫
- 大体の場合2秒もいらないのだが、運が悪いと詰むっぽいので、適当に打ち切って3回実行する
- 27枚 * 2に分割するときに、既存の4枚が2枚ずつ割り振られないようにする。すると既存の組合せが出てこなくなる
- 本番は $A_i$ は一様ランダムだったのだが、中心の方が密度が高い分布の方が性能が高かった。(GPTに聞いて出てきた)三角分布というやつを採用。
おまけ: なぜ2段階調整がよいのか?
まず、1段階調整だと何が難しいのかを考えます。
そもそも満点は取れるのか?500枚を8枚 * 50グループに割り当てる方法は
$$ \frac{500!}{(8!)^{50} \times 100!} \approx 2^{2477} $$
であり、入力が $42 \times 50 = 2100$ bit程度であることを考慮すると、実は(無限の計算時間があれば)満点が取れそう。
計算時間の方はどうか?この解法は基本的に「当たり待ち」解法であり、仮に最高効率でbirthday attackをしたとしても、$O(N \sqrt{E} \log {E})$ 程度の計算量は必要そうです。よって、$\sqrt{E} \approx 10^{6}$であることを考えると、満点はかなり苦しそう(が、こうして書いてみるとハチャメチャに定数倍高速化すれば不可能ではない範疇な気もする?)
また、この問題には「指数的に調整する」という別方針もあります。例えばカードとして $10^{15} - 10^{12}, 10^{12}, \frac{10^{12}}{2}, \frac{10^{12}}{4}, \cdots$ を用意していけば、誤差をどんどん半分にできる…というような方針です。これは計算時間は爆速である一方で、精度を出すにはカードが大量に必要になります。
つまり、
- 当たり待ち: カードが少なくても精度が出る / 計算時間が大量に必要
- 指数的に調整: カードが大量に必要 / 計算は爆速
となり、これを眺めると2つの方針のハイブリッドをやりたくなり、自然に2段階調整の方針が出てきます(…と後付けで納得しましたが、本番中は特に何も考えていませんでした)。
実際に、2段階調整だと各段階の自由度は
$$ \frac{250!}{(4!)^{50} \times 50!} \approx 2^{1192} $$
であり、$1050$ bitに対してはまだ余裕があります。ていうか $2477 \to 2384(=2 \times 1192)$ で8枚割り当てからあんまり自由度減ってないんですね。
計算時間はどうでしょうか?最終的に一回のイテレーションで $C(27, 2)^{2} = 123201$ 個の値を試しているので、$\frac{123201}{\sqrt{E}} = 8.7$ % 程度で調整済みの値が出てくると見積もれます。よって、雑には50倍して600回程度のイテレーションで全部調整可能というすごい計算になります。実際には
- $123201$ 個の値には大きすぎる / 小さすぎる ものが含まれる
- 使えるカードが毎回入れ替わる仮定だが、そんなことはない
などで更にイテレーションが必要になっていると考えられます。それでも直感的には10万回もイテレーションは必要ない気がしており、もしかしたらさらなる改善が出来るのかもしれません。