ICFPC2022 参加記

ICFPC2022に出場しました。 結果は151人中33位でした。(順位表)

私が知る限り日本からの参加者はほぼ全員私より上ですが、あまりICFPCに関する記事は多くないように思いますし、折角なので参加記を書くことにしました。ためになる内容ではありませんが、楽しんで頂ければ幸いです。

問題概略

Specification等は主にこちらで公開されています。

https://github.com/icfpcontest2022/icfpcontest2022.github.io

特に、このpdfが最終版のSpecificationです。

https://github.com/icfpcontest2022/icfpcontest2022.github.io/blob/a38d452791b43cf2b32cd89447c39ca4fe606b09/ContestSpecification_v2_4.pdf

細かい仕様等は省きますが、概要だけざっくり述べると以下の通りです。

  • 縦横400pxの画像が目標として与えられる
  • 同じく縦横400pxのcanvas(1つの真っ白なブロック)を、以下の操作を用いて出来るだけ少ないコストかつ高いクオリティで目標の画像に近づける
    • Line Cut Move 縦方向もしくは横方向にブロックを分割し、新しいブロック2つにする 基本コストは7
    • Point Cut Move 1点を中心としてブロックを四象限に分割し、新しいブロック4つにする 基本コストは10
    • Color Move ブロックを任意の色で塗る 基本コストは5
    • Merge Move 同じ形の隣接したブロック同士を交換する 基本コストは3
    • Swap Move 同じ辺の長さの隣接したブロック同士を結合する 基本コストは1
  • さらにこれらのコストは、操作ごとに size(canvas)/size(block) が乗算される(つまり、ブロックのサイズに反比例する形でコストが増えていく)
  • 最終的なスコアは、これら操作群によるコストの値と、目標画像との類似関数の値の和で、これを最小化することが目的

(ICFPCは例年仕様の追加や変更が行われるのですが、私はそれらを全く追いきれなかったので、それらは省略します)

与えられた画像の一例は以下のようなものでした。

ICFPC2022_Problem_Pictures.jpg

私の推し画家であるアルチンボルドの絵(下段右)も入っていて、テンション上がりました。 また、中段右の青い画像は、ゴッホの『星月夜(ほしづきよ)』だそうです。よく目にする画像ではありますが、今回初めて名前を知りました。英語では”The starry night”というらしいです。かっこいいですね。

解法概略

私の解法の概略を述べます。

非常に重要な点として、各操作のスコアは size(canvas)/size(block) が乗算されるということがあります。具体例をあげると、一辺10pxの正方形は、$\frac{400\times400}{10\times10}=1600$ 倍になります。最終的なスコアは大体10000から40000程度になるので、このサイズの正方形に色を塗るだけで、もうほぼその値になってしまいます。 (極めて当たり前のことですが、私は中々この事実の重大性に気付けませんでした)

この性質により、大きいブロックを保持したまま操作していくのが大切なので、 Line Cut Move or Point Cut Move (細分化) → Color Move (色を変更) → Merge Move (元のサイズに戻す) という操作を繰り返すことで、そのことを達成しています。

特に、Cutのパートに関しては、以下のように2回のPoint Cutを使用することで、任意の矩形領域を塗ることが出来ます。(あえて小さめに矩形領域を表示していますが、先述の通り、このサイズの矩形領域は実際には作りません)

cut_part

また、矩形領域の辺がcanvas全体の外枠に接する時にはCutの回数を減らすことが出来るので、それらを丁寧に場合分けしながら、上記のCutの種類やMergeの順序などを適切に試すことで、出来る限り少ないコストで矩形領域を塗る関数を実装しました。(これの実装にはかなり苦労しました。)

そして、この関数を使用する上で、どの矩形領域を塗っていくかということは本問において本質的になります。

ランダムな矩形領域を試して、最もスコアが改善するものを貪欲に選択していくというアルゴリズムを最初に試しましたが、それでは色々と無駄が生じます。

例えば、以下の画像が入力の場合を考えてみます。 9.png

直感的には外側から白色→黒色→灰緑色→モナリザの部分と塗っていくのが最適そうに思えますが、このような単純なアルゴリズムでは先に形としてまとまっているモナリザの方を塗ってしまったり、灰色と黒色の中間色で部分を塗ってしまったりします。

特に、以下のチェス盤を、白と黒の中間色である灰色で塗ってしまうという実行結果が多発しました。

ICFPC2022_Problem1.jpg

このことを改善する案を色々考えましたが、私にはどうにも思いつかず、結局手動ツールを作成して手動でその矩形領域を決めるという方針をとりました。

矩形領域の決め方は、モナリザの例のように外から中へということも注意すべきですが、後から上書きされるなら出来るだけ大きな矩形領域の方が良いという事にも注意する必要がありました。

以下がその一例です。

ICFPC2022_Problem2.jpg

上段では、ロボットの足をそのままの形で塗っていますが、下段では足をもっと長く描いて、その後胴体部分を上塗りすることにより、小さい矩形領域を扱うということを避けています。

このことによって、操作にかかるコストをより抑えることが出来ます。

(また、この手動解を作成していく中で、さらなる改善に気付いてしまいました。上位陣がとっていたDP解と似た方針です。手動でやっていく中で、こういうアルゴリズムを上手いことを実装すれば良かったのかとなりましたが、流石にそれをする時間は全くありませんでした。)

そして、更にその手動解を乱択山登りで改善していきます。

例えば、手動解では当然ブレが存在するので、先のチェス盤などだと線が少しズレるということが発生してしまいます。人の目では完全に一致しているように思えても、類似関数の値は0でないということが良くありました。

そこで、手動解自体にランダムな変形を加えてスコアがどうなるかを見るという、乱択山登りを行いました。また、山登りと言っても、手動解には先程述べた貪欲アルゴリズムをプラスしているので、各実行にも少しブレがあり、かなり楽々と局所最適解は抜け出してくれているようでした。

これらを合わせると、チェス盤の出力は以下のようになって、スコアは20596でした。

image.png

ちなみに、参加者全体のベストスコアは5013らしいです。エグ…

なお、手動解を改善する乱択山登りには、色に関するランダム性も追加しています。 例えば、以下のTheの部分が一番分かりやすいと思うのですが、この部分は目標画像の色よりも少し薄くなっています。(他の色も、スポイトツールで見ると実はかなり違います) 色を背景まで考慮して塗るようにすると、よりスコアが改善されました。

ICFPC2022_Problem8.jpg

私の方針としては以上です。

結果

問題は全部で40題ありましたが、その内私が主に取り組んだ、1日目の時点(Lightning Divison)で公開されていた25問に関しては、以下のスコアが得られました。

seedscoreseedscoreseedscoreseedscoreseedscore
12059669301113607016272192125295
25627722274121001617421792227297
315575822337131404318414352331036
418468913964142617419337742423799
5188231031342153064920249002534244

これらの合計値は606437です。

iwiwiさんのこのツイートにある画像によると、どうやら1日目時点における6位相当のスコアだったようですね。それ以外の問題がボロボロでしたが、善戦できた方では? と思っています。

同時に、Unagi(Chokudaiさんやwataさんのチーム)のヤバさも伝わってきますが… (1日目で401296って何???)

他の方々の解法

優勝したUnagiのレポジトリは以下で公開されています。DP解などが紹介されていますが、流石すぎるといった感想です。

https://github.com/icfpc-unagi/icfpc2022

また、これはICFPのDiscord上で共有されていた話で、(ネットリテラシーの関係上)直接載せることはできないのですが、26問目から35問目までのグリッド状の問題に対して、非常に上手いブロック同士のマージの方法が紹介されていて感心しました。

これらの問題は一度全てのブロックをマージする必要があるのですが、あえて途中で切断を挟みながらマージしていくことで、小さい矩形領域を扱うのを避けられ、より少ないコストで操作を達成していました。

全く考えもしなかったことですが、これが出来ているとスコアに対する影響が大きいので、もっと順位も上がっていたかも知れません。悔しい。

[追記] そのチームによる解説記事があがっていました。 こちらから見ることが出来ます。

マージ方法以外にも、色々なるほどという内容が記載されていました。

https://teletype.in/@bminaiev/icfpc-2022

その他感想など

今回は問題の提出をAPI通信で行う事が可能だったので、それをしようと思ったのですが、整備が大変でした。説明がその時点では公開されていなかったので、TypeScriptを使って手探りでやったらうまく行かず(ファイル読み込み時のasync/await関連でかなり詰まりました)、結局Pythonを使ったらすんなりいきました。また、入力もpngやJSONでtxtではないので、それをデータ化するのにもPythonを使用するなど、思ったよりもPythonやBashを使用しました。

また、TypeScriptのサンプルコード的なものをC++に導入する際に、structの型変換で詰まりました。

export type Instruction =
  | NopInstruction
  | CommentInstruction
  | ColorInstruction
  | PointCutInstruction
  | VerticalCutInstruction
  | HorizontalCutInstruction
  | SwapInstruction
  | MergeInstruction;

こういう型定義がTSのファイルにあり、「確かにTSではこういうコードをよく書くけど、C++でどうやって書くんだ…?」となり、std::disjunctionが(実際に関数で使用する上では)それに近いのかと思って調べていましたが、結局まだよく分かっていません。(参考)

TypeScriptは結局のところJavaScript、つまり動的型付け言語なので、asやanyでどうにでもなるのですが、C++は流石にそうもいかず、virtual(仮想関数)とoverrideで何とかなるかと思いきや、なりませんでした。(StackOverFlow曰く、私のやろうとしていることは不可能だと書いてありました。別の方針もあるのかも知れませんが…)

この辺のグダグダは、かなり反省点です。公式のコードもあくまで参考程度に留めるべきだったかも知れません。

ビジュアライザに関して言えば、公式のPlaygroundにあるのを、自分側のReactに移植しました。8月に自作ビジュアライザを準備した甲斐が少しはあって良かったです。特に、公式のコードがエラーメッセージの部分でJSON.stringify()を忘れていて(?)、その部分を正確なエラーメッセージが出せるように直せたのは良かったです。 尤も、仕様が変わってしまったので、このビジュアライザは結局殆ど使用しませんでした。 めちゃくちゃアニメーション機能的なのも欲しくなりましたが、それは用意しておらず、準備不足でした。

Reactではコンポーネント間の情報の受け渡しが少し難しいので、このくらいの規模感だったら素のTypeScriptの方が速いとなり、ゼロから実装したのですが何とかなってよかったです。(逆にこれが出来ていなかったらもっと破滅していました)

以上です。 お読みいただきありがとうございました。