TERRYのブログ

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

MC Digital プログラミングコンテスト2025 (AHC048) 解説

AtCoder Heuristic Contest 048お疲れ様でした。相対スコア858,336,697,107点で2回連続の5位です!

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

ビジュアライザ (seed=0, score=4,012点)

問題概要

絵の具を混ぜて欲しい色を作ってね。

atcoder.jp

解法

概要

T の値によって4通りの解法を使い分けました。

T が中途半端な値の場合、途中まで上位解法を行った後、ターンが足りなくなったら下位解法に切り替えました。

  • T\sim 4000: 固定パレット上で混ぜる
  • T\sim 13000: 1色1レーン使って、4色を b/aa は定数)ずつ混ぜる
  • T\sim 19000: 1色1レーン使って、4色を p/q ずつ混ぜる
  • T\sim 33000: 1色1レーン使って、4色を (b+p/q)/aa は定数)ずつ混ぜる

前提

指定された色 (C, M, Y) を作っていく問題ですが、各色を3次元空間上の点だと思うと見通しが良くなります。

3次元空間

絵の具の色を表す点 A, B, C, D を適切に4色選ぶと、四面体 ABCD 内部の点は(任意の実数比で混ぜ合わせることができれば)自由に作ることができます。

作成したい色を表す点を P とすると、点 P が四面体 ABCD の内部にあれば点 P を作ることができます。これは四面体 ABCD の体積と、4つの四面体 PBCD, APCD, ABPD, ABCP の体積の和が一致しているかどうかを調べれば良いです。*1 *2

このときの四面体 PBCD, APCD, ABPD, ABCP の体積比を a:b:c:d とすると、絵の具 A, B, C, Da:b:c:d の割合で混ぜることで所望の色 P を得ることができます。*3 *4

ということで、 T が大きいときはできるだけこれの達成を目指します。

4色の絵の具の色を頂点とする四面体

① T~4000くらいのとき

T が小さいときは上記の4色混ぜをするだけの手数を確保できないので、たくさん小部屋を作って適当に混ぜました。

レーンを N 本用意して、レーンを縦に2分割したセルを作り、各レーンの中で絵の具を混ぜました。操作の候補として、

  • レーン i に対して、仕切りを動かさず、0, 1, 2, 3, 4g の絵の具を足してから採取する
  • レーン i に対して、仕切りを削除し、適当な場所に動かした上で、0, 1, 2, 3, 4g の絵の具を足してから採取する

を使い、ビームサーチしています。

レーンとセル

絵の具をどのように足せば良いかについて考えます。セルに既に入っている絵の具の色ベクトルを \boldsymbol{x} 、量を w とし、目標とする色ベクトルを \boldsymbol{y} とします。このとき、追加する絵の具の量を \Delta w\ (=1, 2, 3, 4) とすると、追加したい絵の具の色ベクトル \boldsymbol{z} は、

\boldsymbol{y} = \dfrac{w \boldsymbol{x} + \Delta w \boldsymbol{z}}{w+ \Delta w}

より、

\boldsymbol{z} = \dfrac{(w + \Delta w)\boldsymbol{y} - w \boldsymbol{x}}{\Delta w}

となります。あとは \Delta w gの絵の具を混ぜてこれに近い色を作る方法を高速に探せばよく、予め \Delta w gの絵の具の混ぜ方 {}_K H_{\Delta w} 通りを前計算してkd-treeに格納しておくと、 \boldsymbol{z} に近いいい感じの色を O(\Delta w\log K) で検索することができます。*5

小部屋解法

② T~13000くらいのとき

T が大きくなると少しできることが増えてきます。ここから、前述の4色混ぜを行っていきます。

まず K 本の絵の具のタンクを用意し、それぞれに1gずつ絵の具を入れておきます。各タンクから好きな量だけ絵の具を取り出すことができれば、所望の色を作り出すことができます。

4色混ぜパレット

これを実現するため、下図のようにタンクの内側に仕切りを追加してから外側の仕切りを開けるという操作をします。これにより、タンクの中の絵の具を 1/a 刻みで取り出すことができます。なお、絵の具が足りなくなったときは仕切りの位置を最大までリセットしてから1g足します。

好きな量だけ取り出す方法

これで各色を好きな量だけ取り出すことができるようになりました。これを4色について繰り返して混ぜます。欲しい量ピッタリ出すことは基本的にできないので、色ごとに欲しい量より小さい側と大きい側の2通りを考え、bit全探索で 2^4 通り全部調べることにします。

これを使って、各目的色に対して4色ずつ選んで混ぜることを繰り返すビームサーチをします。*6 なお、混ぜて採取した後の絵の具が微妙に残ってしまいますが、私の解法では毎回余った分は捨ててしまいました。*7

これにかかる手数を考えます。目的色1色あたりにかかる手数を考えると、

  • 絵の具を足す: 平均 1 手(合計で H 回くらい追加するため)
  • 絵の具を採取する: 1
  • 絵の具を捨てる: 1
  • 絵の具を出す量を調節する: 2\times 4 = 8 手(1色あたり 2 手を 4 色分)
  • 絵の具を足すときに仕切り位置のリセットを行う: 平均 2

となるため、T13H=13000 くらいからこの解法が使えることになります。

4色混合解法その1

③ T~19000くらいのとき

T がさらに大きくなると、より精緻に取り出すことができるようになります。下図のように、一旦タンクの長さを伸ばしてから取り出すことを考えます。

分数方式の取り出し方

タンクの長さを L 、現在の絵の具が占めるマスの数を a とすると、 a\le p\le q\le L であるような有理数 p/q を作ることができます。このようにすると、取り出す量を概ね 1/L^2 刻みくらいのオーダーで制御することができ、精度が向上します。

もう少し詳しく解析しましょう。簡単のため a=1 のケースを考えると、1\le p\le q\le L を満たす既約分数 p/q の数は、オイラーのトーシェント関数を \varphi(n) として、 \Phi(L)=\sum_{k=1}^L\varphi(k) と表すことができます。これは \Phi(L)\sim \frac{3}{\pi^2}L^2+O(L\log L) と近似できる*8 ので、ざっくり平均 3/L^2 刻みくらいで量を調整することができることになります。*9

この解法にかかる手数を考えます。目的色1色あたりにかかる手数を考えると、

  • 絵の具を足す: 平均 1 手(合計で H 回くらい追加するため)
  • 絵の具を採取する: 1
  • 絵の具を捨てる: 1
  • 絵の具を出す量を調節する: 4\times 4 = 16 手(1色あたり 4 手を 4 色分)
  • 絵の具を足すときに仕切り位置のリセットを行う: 平均 2 ← これは不要

なので、T19000 くらいからこの解法が使えることになります。

4色混合解法その2

④ T~31000くらいのとき

T がかなり大きいときはもう1段階ステップを増やしました。

下図のように、各タンクの片側だけでなく両側から出せるようにしておきます。左側から大雑把に出して、必要に応じて右側への「繰り下がり」を行った後、右側で分数方式を使って微調整をする帯分数的な制御をしました。*10

このように制御すると、 (b+p/q)/a を作り出すことができます。かなり良い精度が出せて、色の誤差は 10^{-5} くらいまで小さくすることができます。

帯分数方式の取り出し方

4色混合解法その3

その他の工夫

焼きなましによる無駄な絵の具の削減

D が大きいケースはできるだけ最後に余る絵の具を少なくしたいので、ビームサーチを1000ターン行った後、最後の20ターンだけ焼きなましを行いました。

十分精度よく絵の具を制御できると仮定して、余った絵の具は毎回捨てることにすると各ターンが独立になるため、各ターンでどの絵の具をどのくらいの量使うかを焼きなましました。

最後の20ターンはビームサーチと焼きなましの両方を試し、良かった方を採用しています。

使用する絵の具数の削減

絵の具は K 色与えられますが、パレットの大きさは固定なので、絵の具の種類数が多すぎると1色あたりに使えるマスの数が少なくなり、制御の精度が落ちてしまいます。そのため、解法①以外では不要な絵の具を予め削除しておきたい気持ちになります。*11

3次元空間上で K 個の点を考えると、これらの点の凸包を構成する点だけあれば凸包内部の点を全て作ることができます。そのため、そのような絵の具だけを残して他の絵の具をなかったことにしました。*12

3次元凸包の実装は、CodeforcesブログO(n^2) のアルゴリズムを参考にしています。

感想

自由度がものすごく高くてとても面白い問題でした。個人的には Graphorean の次に好きな問題です。

4色を混ぜるところまでは割とすぐに思い付いたのですが、それだけだと順位がズルズル落ちていったので、どうやって精度を上げるべきか相当悩みました。しばらく k 進数を考えていたのですがそこまで精度が上がらず、また手数もかなり消費してしまうので苦しかったです。分数解法は最後の土曜深夜に思い付いたのですが、それで精度が爆上がりしたのがかなり面白かったですね。

これで1ページ目は9回連続となりました。せっかくなら2桁に乗せたいですが、次回は短期コンテストなので相性次第といったところでしょうか。あまり気にせず頑張ります。

直近のコンテスト成績

*1:ってChatGPTが言ってました。

*2:浮動小数点数の数値誤差には注意してください。

*3:ってChatGPTが言ってました。

*4:連立方程式を立てて解いても良いです。

*5:絵の具の混ぜ方の組合せ数が少ないときは愚直な線形探索の方が高速なので、適宜切り替えました。

*6:「1手操作する」ではなく、「1色分混ぜる」操作列をビームサーチの1ターンとしました。

*7:Dが大きいケースでは1gでも捨ててしまうと上位と大きな差が付いてしまうので、なんとか頑張って捨てないようにすべきでした……。

*8:https://en.wikipedia.org/wiki/Totient_summatory_function

*9:……というのは後付けで、コンテスト中はここまで細かく解析していません。

*10:分数を考える前にもともとk進数で制御する方法を考えていたので、その名残で繰り下がりと呼んでいます。

*11:解法①では4色混ぜるだけの手数の余裕がないので、絵の具の色が多い方が有利かなと思います。

*12:凸包を構成する点も必要なわけではないのですが、そのような点を削除することを失念していました。コンテスト序盤で考察はしていたのですが……。