sankantsuのブログ

技術メモ・競プロなど

AHC032 参加記

2024/04/07 19:00 - 23:00 に開催された AHC 032 に参加した。 この記事では今回の参加について振り返る。

問題概要

atcoder.jp

N x N (N = 9) のグリッド上に数字が書かれている。 グリッド上の 3 x 3 領域を選んでスタンプを押すことで、スタンプに書かれている数字をグリッド上の数字に足すことができる。 最終的に、グリッド上の各マスの数字の mod 998244353 の和を最大化したい。

結果

最終 87 位 (パフォーマンス 2130)

前回 (AHC 031) に続いて 2 桁順位。 短期コンでは今までで一番の成績になった。

解法

基本で貪欲左上から合わせていく。 左上の 6 x 6 はスタンプの左上位置を合わせて押したあとの mod が最大となるように 1 回だけ押す。 右上の 6 x 3 ブロックは、各行あたりスタンプを 3 回ずつ使って mod の和を最大化。左下 3 x 6 も同様。 最後に残る右下 3 x 3 ブロックはスタンプ 6 回使って mod の和を最大化。

この配分だと 3 回分スタンプが余るので、余った分は左上ブロックに適当に配分。 余りの分配先のマス集合を変化させて山登りで時間いっぱい回す。

実行例 (seed = 0)
実行例 (seed = 0)

提出コード

https://atcoder.jp/contests/ahc032/submissions/52152178

経過

まずは適当なランダム解法とスコア計算を実装。 最初問題文を読み間違えて mod の和ではなく 和の mod の最大化だと勘違いしていて、スコア計算が合わないことで気づいた。

焼きなましできなくはなさそうだが、1 回の操作の影響範囲が大きいので焼きなましだと近傍つくるのが難しそう。 焼くにしても、とりあえずある程度良い初期解をつくっといたほうが良さそうだと思ったので、とりあえず貪欲で良さげな解をつくる。

一回合わせた場所は後から触らないようにすることで、左上から 1 マスずつ合わせていくことができるので、その方針で実装。 まずは右下の L 字領域は無視して左上 7 x 7 マスで 1 回ずつスタンプ押して合わせていく方法で実装。 さらに、右下の L 字について 3 列ずつや、3 x 3 ずつ各スタンプ 1 回で最大化する実装を書く。 スコアは 10.70T。 ここまでで 59 分。

だいぶ実装がコピペで膨らんでいたりしたので一回リファクタリングをはさむ。 まだこの時点では、まとめて最大化している部分で赤に近いスコア低いマスが目立っていたので、こちらに余りのスタンプ回数を使いたい。 そこで、同じ場所に複数回スタンプを押す際のパターンを全列挙して一番良いパターンを選択する実装を書いた。 スタンプの回数の配分などが概ね落ちついた時点でスコア 11.49T。 ここまでで 133 分。

この時点では実行が 48 ms でだいぶ余っているので、左上ブロックでスタンプを 2 回消費してるマスを適当に変えて複数パターン試してみることにする。 最初単純に乱択にしていたが、後から少し工夫してマス集合を 1 箇所ずつだけ変化させる山登りにしてみる。 貪欲パートがまあまあ重く、山登りの繰り返し回数はあまり多くならないので焼きなましまではしなくてもいいかなという判断。これでスコア 11.59T。 ここまでで 179 分。

最後の 1 時間はスコア計測スクリプトを書いてパラメータ調整を主にしていたが、結局スコア上昇はなし。 終盤は同じような提出を複数回やってもスコアが下っているような気が...。終盤は駆け込みで提出が増えて重いのか、繰り返し回数が減ったりしてるんじゃないかという気がしなくもない。

おわりに

今回は短期 AHC ではじめてある程度良い成績が出たので嬉しいです。 上位陣とおおまかには近い方針を引けている気がするけれど、一方でまだ超えられない壁があるようにも感じます。 細かい点を含めて他の解法などを読んでみて、差が出る場所を考えてみたいと思います。

AHC031 参加記

2024/03/22 から 2024/04/01 までの期間に開催された MC Digital プログラミングコンテスト2024 (AHC 031) に参加した。 この記事では今回の参加について振り返る。

問題概要

atcoder.jp

W x W (W = 1000) のグリッドを長方形の区画に分割して、与えられた予約に割り当てる。 このとき、

  • それぞれの予約に対して割り当てる区画の大きさがその予約が要求する大きさを下回るとコストが生じる。
  • 連続する日付に対して区画の区切り線の変更があると、区切り線の長さに応じてコストがかかる。

問題例 (seed=1)
問題例 (seed=1)

結果

プレテスト 89 位, 最終 94 位 (パフォーマンス 2051)

AHC 011 以来 2 回目の 2 桁順位をとることができた。 長方形への分割戦略として比較的良いやり方を早い段階で考えられた点が良かったと感じている。

最終的な解法

最終的な解法はおおまかには次の 3 ステップからなる。

  1. グリッド全体をいくつかの横線で区切る (この横線は全ての日程で共有する)。
  2. 1 でできた横長の長方形内部に縦線を引いて日ごとの予約に対する区割りを決定する (焼きなまし)。
  3. 隣接する日付について、縦線を少し移動させることで縦線を再利用できないか試す。

1 では、s_k を「各日付の k 番目に大きい予約のうち最も大きいもの」としたとき、上から順に s_1, s_2, ... の大きさの長方形を確保するように線を引いていくことにした。 このようにした意図は、特定の日付に大きめの予約が固まって出てきたとしても要求を満たす区割りが実現できるようにと考えたためである。 1 でつくった横線は全ての日付について動かさず必ず再利用されるので、この部分については区切りの変更によるコストがかからない。

2 ではとりあえず縦線の再利用については考えずに日ごとに独立に、各予約を面積要求を満たすように 1 でつくられた行に割り当てる方法を考える。 これは、問題文で与えられたスコア関数をほとんどそのまま使って焼きなましでつくる。 近傍としては、以下の 3 種類を用いた。

  1. 予約を 1 つランダムに選んで割り当てる行をランダムに移動させる (20.0%)。
  2. 予約を 2 つランダムに選んで割り当てる行を交換する (79.7%)。
  3. 行をランダムに 2 つ選んでそれらに含まれる予約を貪欲に割り当て直す (0.3%)。

計算時間のほとんど (2800ms) をこの焼きなましステップで使っている。

最後に、3 で縦線の共通化によるペナルティ削減を試みる。 まず 2 で求めた各日付の配置について行ごとに小さいほうの予約から順番に左から詰めていって日ごとに仮配置を決める。 その後隣接する日付の配置を見て、縦線を少し右にずらすことで予約に対する面積不足を発生させることなく縦線を共通化できるならそれを採用する。 「i 日目の配置を j 日目の配置を見て変更する操作」を (i, j) と表わすとき、(1, 2) → (2, 3) → ... → (n-1, n) → (n, n-1) → ... → (2, 1) のような順で操作を一通り繰り返すというのを、5 周分行った結果を最終的な結果とする。 複数周分回すのは、一度合わせたつもりの区切りが参照先としていた配置自体が後から変更されてずれてしまった場合に区切り位置を合わせなおすために行っている (つもり)。だいたい 3 周もすれば基本的に配置が動かなくなった。

以下に、実行例を示す。

seed=0 の実行例 (score: 860)
seed=0 の実行例 (score: 860)

seed=1 の実行例 (score: 73385)
seed=1 の実行例 (score: 73385)

提出コード

https://github.com/sankantsu/competitive-programming/blob/master/ahc/ahc031/main.cc

日記 + 考察過程

03/22 (金)

前回の AHC 030 から競プロに復帰していて、今回は復帰後 AHC 2 戦目。 開催開始後に気づいた前回と違って今回は始まる前から楽しみにしていた。

初日は出先だったのでコードは書けないが、とりあえず問題を読んでおく。 ざっと読んだところだと、最終的に面積不足のペナルティと区切り線変更のペナルティがどのくらいの比率で効いてくるのかよくわからないが、とりあえず面積不足のペナルティは最低限減らせるようにしないと厳しそうである。 余剰スペースがほとんどないとして、面積不足を発生させないように長方形をつくり隙間なく敷き詰める問題を考えてみるが、結構難しそう。

その後で入力生成方法をよく読んでみると多少スペースの余りが出るように入力がつくられていることがわかり、一番簡単なケースでは平均で 1/4 も面積が余るようにできているらしい。 前回 AHC 同様に相対スコア制なので簡単なケースでベストに近い点数を出すのは重要だし、難しいケースで面積不足ペナルティが発生するのは多少許容してとりあえず面積余りが出やすい条件をターゲットにして、線を再利用しやすい配置を考えてみると良いかもな、というぐらいの考察をしておいて実装はせずに初日は終了。

03/23 (土)

前日の考察もふまえて実装を行っていく。

最初にやったのは、サンプルの移植と、サンプルについて長方形の縦幅を予約を満たすように広げるようにした拡張版。 それに続いて、スコア計算を問題文どおりに実装した。 素直に定義通り一般の配置に対するスコア計算をやると結構計算が重いので、後々焼きなまし等にもっていくなら差分計算できるように制限して解を考えるのは必須だろうと感じた。

ここまでの考察をふまえて、

  • いくつかの線を再利用し続けられる
  • スコアの差分計算を行いやすい

という点を念頭に、配置方法を日が変わっても動かさない横の固定線 + 日ごとに動かす縦の変動線の組みあわせで構成する、いわゆる「短冊配置」戦略を中心に考えてみることにした。 縦線の隣接日程での共通化は念頭にはあったが、とりあえずは日ごとに独立に面積不足を出さない配置ができないか考える。

固定線の配置については最低限、全日程で最も大きい予約を入れられる大きさは確保しなければならない。 そのように確保した区画に仮に各日付で最も大きい予約を詰めるとしたら、次に面積不足に陥いる可能性があるのは「各日付の中で 2 番目に大きい予約のうち全日程を通して最も大きいもの」であると考えたので、これに合わせて次の固定線を決める。 このような調子で「各日付の k 番目の予約のうち最も大きいもの」に合わせて固定線をつくれるところまでつくるという方針で固定線を引くことで面積不足が発生しづらい固定線の引き方が達成できそうだと考え、この方法を採用することとした。 結局この固定線のつくりかたは最終解法までそのまま引きつがれることになる。

縦の変動線については、各日付ごとに独立に 予約 id -> 割り当て行 の対応を解くことで決定する。 具体的には、各日付について最も大きい予約から順に予約を満たすことのできる行のうち最も残り面積が小さいものから順に割りあてる貪欲法を採用した。

ここまでで横線を完全に横線を再利用しつつ、面積余りを発生させる頻度がそれなりに少ない解法を組むことができたので提出してみると、一瞬だが 9 位という好位置につくことができた。 一瞬でもこのような順位をとれたことは無かったのでこれは嬉しい。

03/24 (日)

この時点でわりと方向性としては悪くない解法を引けている感覚があったので、さらに改善すべく変動する縦線の隣接日程間での共有に考えをうつしてみる。

隣接する日付の予約で面積が近いものがあれば、小さいほうの予約を大きいほうの予約の面積に合わせることで区画の大きさを共通化できるはずである。 この考えに基づいて、隣接する日付の予約から面積の差が最も小さいものからいくつかペアをつくる。 そのうえで、ペアにしたものについて同じ区画を優先的に配置した後で、残りをこれまでと同様大きい予約から順に貪欲に割り当てるという解法を試してみる。

しかし、この解法はうまくいかなかった。 うまくいかなかった原因は、より大きい予約よりもペアになっている小さい予約を配置してしまうために、後から大きい予約を詰められないケースが出てきて面積不足ペナルティでスコアが大幅に悪化してしまっていたためだった。

結局この日は提出は行わず。

03/25 (月) - 03/29 (金)

今回は結局平日は全く時間がとれず、たまに考察はしてみるものの実装は全く進まなかった。

03/30 (土)

一度は 9 位まで上がった順位もほぼ丸 1 週間も放置したので 220 位台ぐらいまで下がっていた。 縦線の共通化は結構難しそうかつできたとしても少し共通化できるぐらいだと限定的な効果しか無いだろうと感じていたので、一度立ちもどって日ごとの縦線の配置を貪欲解法ではなく焼きなましで改善するのを試みることにした。 これでより面積ペナルティを少なくすることと、縦線の長さの合計を減らすことを狙う。

解としては各予約に対して割り当て先の行を対応させる長さ N の配列を持つことを基本として、スコア計算を O(1) にするために行ごとの割り当てた予約の数と割り当て面積の合計を同時に更新する形とした。 近傍は、最初予約の割り当て行の移動と swap の 2 種類で実装した。

近傍が上の 2 種類だけだと面積不足が発生していても局所解から抜け出しにくい傾向があるように感じられたので、もう少し大幅に状態を変化させる近傍として、2 行選んでそれらに含まれる予約すべてを貪欲に配置しなおすという近傍を追加した。 この近傍は他と比べて計算が圧倒的に重いので、iteration 回数を確保するために使用頻度はかなり小さく設定したが、それでもスコア上昇の効果はあったようである。

焼きなまし解法を使うことで、正確な順位は覚えていないが 70 位台ぐらいまで回復した。

03/31 (日)

日ごとの区画方法決定部分の焼きなまし化もとりあえずできたので、改めて縦線の共通化に取り組んでみる。 共通化を優先して先に配置したりしていると面積要求を満たせなくなって本末転倒になることを先週の実装で学んでいたので、今回は縦線の共通化はあくまで補助的な手段で、運が良いときだけできれば良いぐらいの感覚で行ってみることにした。

イデアとしては、隣りあう日付で偶然近い位置に縦線があったら一方をずらして同じ位置まで持っていけないか考えてみるというものである。 より具体的には、隣りあう日付についてある行の配置を取りだしてきて、一方の配置の縦線位置を左から見ていったとき、もう一方の配置内に存在する縦線位置に重なるところまで右方向に動かせるなら動かすという貪欲法で縦線を重ねていくことにした。 一回の処理では隣接する日付しか扱えないので、これを全ての隣接する日付のペアについて繰り返していく。

最後に、visualizer や繰り返し数のログを観察しながら焼きなましのパラメータを調整する作業をして最終提出とした。

04/01 (月)

改善のアイデアとして

  • 細い長方形区画が連続する配置が出現するなら区切りの方向を変えることで長さを削減する
  • 縦線の共通化を試す前の仮配置として面積順に詰めるのではなくシャッフルしてみるなどして探索パターンを増やす

といった候補を考えていて、余裕があったら試してみようと考えていたが、結局実装する時間がとれず試さなかった。 最終日はときどき順位表を開いて順位が下がっていくのを眺めるだけになっていた。

終了後の感想・反省点

上位陣の解法を見ても配置方法はいわゆる「短冊型」が多く、おおまかな配置戦略としては当たりを引けたかなという感じがした。 ただ、一見似たような短冊型であっても内部の配置を決めるための方針には意外とそれぞれの参加者ごとに工夫があって、スコアもかなり差がついているという印象だった。 解説放送で説明していた固定線と中身の配置の 2 重の焼きなましや、変動線部分の共有を行うための焼きなましなどにつても、いずれも自分では思いつかなかった部分であり面白いと感じた。

これらの情報を見たうえで考えたところスコア影響がありそうな反省点としては、以下などがあげられそうである。

  • 横の固定線配置を貪欲で決めうったまま最後まで改善しなかった。

固定線の配置を今回の自分の解法で割りふると、固定線の配置間隔が必要以上に離れたものになってしまうことがあるように思われる。 これによって、縦線一本あたりの平均コストが必要以上に高くなってしまったり、区画内部の面積余りが発生しやすく最終的に面積不足ペナルティを解消しづらくなっていた可能性がある。 詳細順位表の情報から、自分の提出は N が小さいケースで弱かったことがわかっているが、N が小さいケースでは特に固定線の配置間隔が広がりやすかったと想像される。

  • スコア 1 の最適解達成可能パターンを落とした。

今回の問題では、一部パーティションを全く動かさずスコア 1 を達成できるパターンが存在する。 このこと自体は固定線配置のための貪欲を考察の延長として気付いていたが、スコア 1 が達成できるパターンを実際の入力ケースの中に探したり達成できているか動作を見たりすることはしなかった。 実際 seed 32 がそのようなパターンに相当することを後から知ったが、自分の解法の動作を見てみるとスコア 1 を達成していないことがわかった。 解説放送内での発言によればシステムテストではスコア 1 可能パターンは 28 個あったらしくこれらを取れていると特に自分の順位帯にとっては無視できないスコア影響がありそうである。

  • ローカルテスト・入力傾向の分析をサボった。

今回はコンテスト自体にあまり時間がとれなかったこともあり、基本的に解法の評価を seed 0 - 5 あたりの visualizer で確認するぐらいしかしていなかった。 ローカルテストを気軽に行えるような手順の整備などは今後の長期コンテストに向けての課題である。 また、入力の分布の分析などもやらなかったので、例えばスコア 1 達成可能パターンの発生確率なども評価できていなかった。

解説などで紹介されている方法をいくつか延長戦で試して、また次の AHC に備えたい。

ARC 173 感想

atcoder.jp

結果

Aのみ 1完 50:22 (+0ペナ) Performance: 1395, Rating: 1321 -> 1329

前回 ABC で冷やしていたが、今回は微増

感想

ARC はもともと苦手意識がある。 A 問題から結構難しいし、B 問題解ける確率は 5 割あるかも怪しい気がしていたので結構レート的に厳しい展開になることが多そうだなあと思っていた。

しかし、前日の ABC は私用で出られないことが決まっていたし競プロモチベが上がっている今 rated で出られるコンテストに出ないというのももったいないと思ったので ARC 173 は出ようと思い、週はじめから少しずつ ARC 過去問を解いて対策していた。

案の状、A から結構苦戦するし B はだいたい難しいし、C に至っては現状解く気すら起きないが、コードを書けない移動中などに延々と脳内 ARC (考察だけ) とかやっていると、そのうちなんだか ARC の問題も面白いかもしれないぞという気がしはじめていた。

A

K 番目の "Neq Number" (桁表示の数字が隣接しない数字) を求める。

Writer が同じ chinerist さんということで、直前に A - Periodic Number を解いていた (デバッグ終わってなかったけど...) ので、なんだか見た瞬間似てるなあと思った。

問題文読んでいきなり、11739090 って Neq Number じゃなくない (1 が連続してる)?? って思って困惑したけど、誰か質問するだろうし、まあとりあえずサンプル合わせにいけばいっかと思って放置 (結局誰も指摘しないの...)。

桁 DP っぽい感じはわかって、その上で "K 番目の Neq Number" より、"M 以下の Neq Number の個数" のほうが桁 DP 的に楽そうだなあと思ったので、"M 以下の Neq Number の個数" を O(log M) で求めて二分探索でいっか、とそこまで大きく迷わず決まる。

桁 DP で考えると基本各桁になにがしか数字が入っていてほしいので、便宜上最上位より上は 0 埋め相当で考えていたが Neq Number 的には最上位に 0 が隣接してても "同じ数字が隣接してる" と見なしてはいけないということに気付かなかったりでしばらくバグらせ、なんだかんだ 50 分かかった。

B

座標平面上の与えられた点を使って三角形をできるだけたくさんつくる。

ぱっと見なんか難しそうだなあという印象。 ただ制約小さめ (N <= 300) なので、N^3 でもいいのかあとわかる。 そこから逆算して N^3 でなんかそれっぽいことできるかなあと思って、全部の i,j,k のペアについて点 k が点 i,j を結んだ直線上にあるか? みたいなのは全部計算できるなあと考えた。

そのあたりの情報使ってあとはなんか貪欲とかで解けるんじゃないかなあと思ったけど、いい感じの貪欲が思い浮かばなくて結局何も書けず。 ちょっとマッチング問題ぽさも感じたけど、グラフ的な考察はあまり役に立たなそう? (むしろ hyper graph な気もする。使えそうな定理あるか知らないけど)

終了後になってから、なんなら乱択山登りでもワンチャン通っちゃうんじゃない? と思って適当に書いてみたら通っちゃってびっくり。これ今回は正当解法なのね... writer の第一想定解ではないっぽいけど。 アルゴだからといってあんまり見込ある解法思いつきそうになかったら、それなりに見込ありそうな乱択考えてみても良いのかなあという学び。

面白い問題だった。

C

数列の左から奇数番目の数が中央値にならないように区間を広げたときの大きさを全部求める。

B がすぐ解けなさそうだったのでとりあえず問題だけ見てみたけれど、やっぱり C は難しそう。 区間だし頑張ればセグ木とか乗るの? とか適当な考察をしてみるも、区間の中身ほとんど全部覚えとかないと区間合併して中央値とか求まらなくない? と思ってやっぱりよくわからないので諦め。

まだ書いていないけど、解説放送聞いて解法のお気持ちは理解したつもりなので、後で勉強がてら書いてみよう。 考察が少し難しいけど、あんまりマニアックな知識がいるわけではないしよくできてるなあと思った。

yukicoder 5020 (Averaging) 感想

yukicoder という有志作成の問題を解くコンテストに参加した。 今回は4時間の短時間 heuristic コンテストで、作問は E869120 さん。

問題

https://yukicoder.me/problems/no/5020

N (=45) 枚のカードの表裏に数字が書かれている。 2 枚のカードを選ぶと、表裏の数字のそれぞれを 2枚の平均に変化させる。 50 回以内の操作で 0 枚目のカードの表裏の値の両方をできるだけ 5 x 10^17 に近づけたい。

結果

39,532,303 点で 27 / 84 位とまずまず

解法

適当な貪欲法 (スコアが高くなるように 0 と何回か交換) で初期解をつくり、ランダムな操作の挿入/削除を近傍とした焼きなましを行った。 改善としては初期解の改善や、近傍の変更として

  • 操作内容のランダムな変更
  • 適当な 2 操作の順番の swap

を行ってみたが、スコアは改善せず。

終了後

writer 解を見てみると、0 と他すべてのカードの交換を基本として操作順の swap を行って焼きなますというものだった。 これだけで 80M 以上出るとのこと。

自分の解と比べて何が良いのかというと、近傍でのスコア変化が小さく設計されているということらしい。 最初のほうの順番の変更はその後の操作で値が均されてほんの少ししかスコアが変わらなくなるということだろう。

実際少しためしてみると、ほとんどチューニングしなくても 78M のスコアが出た。 焼きなましの改善で何をすれば良いかわからなくて止まってしまったけれど、今回のポイントはスコアが極端に変化しないように近傍を設計することだったのかなあと学びを得た。

ABC342 感想

2024/02/24 1 年半ぶりに algorithm コンテストに参加した。

コンテストページ

https://atcoder.jp/contests/abc342

結果

  • ABCDE5完 (71:20) 0 ペナ
  • パフォーマンス 1652
  • Rating 1311 -> 1354

かなり久しぶりに参加していろいろ忘れている気がしたが、意外と良いパフォーマンスが取れて安心している。

https://atcoder.jp/users/sankantsu/history/share/abc342

感想

先日 AHC030 に参加して久々に競プロ熱が高まっており、algorithm も参加するかと思って出てみた。 かなり久しぶりだったので、当日開催前に ABC341 の問題を解いてリハビリしたりしていたが、overflow やら範囲外アクセスやらでかなりミスを重ねていたので、やっぱり競プロ慣れが抜けているなとちょっと心配になるところもあった。 アルゴリズムもいろいろ知識が抜けている感じがある。いろいろな記事を眺めていると、前に勉強したのにどんな内容だったか思いだせないデータ構造などが結構あった。

レーティングが下がるんじゃないかという不安はあったが、それでも参加するのが慣れを取り戻すには一番だろうと思い、参加はしてみることに決めた。 いざ参加してみると、思ったより順調に進みペナルティもつけずに 5完することができて、レーティングも上昇した。 問題の相性が良かったのではないかと感じるが、ひとまず満足のいく結果がとれて安心した。

A

文字列の中で 1 文字だけ異なるものの index を見つける。

解けないことはないのだが、最初どうやって書くか少し迷ってしまい 9 分も溶かしてしまった。 前後 2文字を先読み後読みすることで解決したのだが、後から解説を読んで 2重ループにしているのを見て、計算量気にしないなら確かにそれが楽だよなと学んだ。

B

数字の出現順のクエリについて答える。

列の長さもクエリも少ないので、計算量気にせず愚直に。 特に迷うところなく、5分ぐらい。

C

1文字置換を繰り返してできる文字列を求める。

文字列もクエリも長いので愚直だとだめ。 少し迷ったが、アルファベットごとの変換テーブルを更新していく形で O(σ(Q+N))。 12 分ぐらい。

D

かけ算して平方数になる値のペア数を求める。

平方数を除算して同値分類するというアイデアはすぐに思いついたが、少し実装をバグらせて時間を消費。 17 分ぐらい。

E

特定の駅に到着するための終発の時刻を求める問題。

問題文が長く、パラメタが多いので読解に時間がかかったが、実質的にはただのダイクストラで queue につっこむ探索候補を少し工夫するだけなので、それほど迷うところはなかった。 そんなに難しくない印象だったが、Atcoder Problems で確認すると水 diff 上位ぐらいになっているので思ったより高い。 自分がこういう無駄に問題文が複雑なパターンを読解するのが比較的得意な部類ということだろうか。 26 分ぐらいで実装。

F

ブラックジャック風のゲームで最適戦略をとったときの勝率を計算する。

E を解き終った時点で残り 30 分あったので取りくんでいた。

まずディーラーの行動は確定的であるので、結果の分布が正確にわかるので、ここから考察してみる。 累積和を組み合わせた DP で終了状態のパターン数が正確に計算できることには気付いた。 しかし、その後最適戦略が何であるかは時間内にはわからなかった。

ディーラーの結果の分布から、プレイヤーが特定の値で stay したときの勝率は正確にわかる。 したがって、stay の勝率と draw の勝率が逆転するかどうかを N から下にむかって検証していき、stay するかどうかの境界を定めれば良いと気付く。 draw の勝率も、累積和 + DP で計算できる。

あとは、この戦略をもとに各値で終了する確率をディーラーのときと同様にプレイヤーの終了分布を計算すれば正解できる。 誤差評価はあんまりよくわかってない。

考察に時間がかかったが、自力 AC することができた。数少ない黄 diff 自力なのでうれしい。

AHC030 参加記

2024/02/09 から 2024/02/19 までの期間に開催された THIRD プログラミングコンテスト (AHC 030) に参加した。 この記事では今回の参加について振り返る。

問題概要

https://atcoder.jp/contests/ahc030/tasks/ahc030_a

N x N マスの盤面に M 個の油田がある。 このとき、油田のあるマスを全て特定せよという問題。

油田の大きさと形は事前に全てわかっている。 油田は重なって配置されることもあり、その場合油田が重なっているマスは重なっている数と同じだけの埋蔵量があるとみなす。

問題例 (seed=1)
問題例 (seed=1) 緑枠で囲まれた油田の位置を当てる

インテラクティブな形式の問題で、ユーザーに許されている操作は以下の 3 つである。

  • 1 マス指定して掘り、そのマスの埋蔵量を確定させる。(コスト 1)
  • k (>= 2) マスの集合を指定して占い、その範囲の合計の埋蔵量を推定する。 (コスト 1/sqrt(k))
  • 油田があるマスの集合を解答として提出する。正解すればそのまま終了し、間違えるとコスト 1 を払って続行

油田マスを全て特定しないと基本的に点数がもらえない。できるだけ低いコストで全ての油田マスを特定することを目指す。

結果

プレテスト 149 位, 最終 143 位 (パフォーマンス 1871)

結果は抜きにしても、久しぶりのマラソンはやっぱり楽しかった。 時間かけて考察したわりにはあまり伸ばせず悔しさはあるが、次回以降に生かしたい。

https://siman-man.github.io/ahc_statistics/030/index_m.html によれば、M=4 から M=6 ぐらいのやや小さい範囲が 60 位台で順位としては最も良かったようだ。

最終的な解法

M が小さいケース (M < 9) と大きいケース (M >= 9) で異なる解法を実装している。

M が小さいケースは縦横独立に解く解法。 最初に全ての行・列の占いを一通り行い、占いの結果をもとに縦方向と横方向配置の解集合をそれぞれ独立につくる。 全体解の候補は独立に解いた縦方向と横方向の配置を組み合わせてつくる。 これらの解集合の間で埋蔵量が異なるケースが多いマスを特定して 1 マス堀り、その結果をもとに解をフィルタリングしていく。 基本的には解候補がひとつになるまで1マス掘り + フィルタリングを繰り返し、最終的に出てきた解を提出する。 もし有効な解候補がなくなってしまったら、予測の改善のため行, 列の占いからやり直す。

M が大きいケースは BFS ベースの解法。 こちらは占いと1マス堀りで場所を特定することを諦め、油田があるマスは全部掘って確認するという考え方。 掘りはじめる際の 1マスだけは観測範囲を狭めていく 2分探索的なやり方の占いによって決める。 1 マス油田を見つけたら、後は BFS で広げるだけ。

実行例 (M=6, seed=1)
実行例 (M=6, seed=1)

実行例 (M=19, seed=19)
実行例 (M=19, seed=19)

提出コード

https://github.com/sankantsu/competitive-programming/blob/master/ahc/ahc030/src/main.cc

日記 + 考察過程

序盤

コンテストは 2/9 (金) から始まっていたが、自分が参加を決めたのは翌日の 2/10 (土)。 その日は外出していたのだが、電車の中で atcoder のサイトを開いていて長期 AHC をやっていることに気付き、なんとなく久しぶりに参加してみようかなと思った。 競プロ自体かなり久しぶりで、最後に参加したのは algorithm 形式は 2022/06/26, heuristic 形式は 2022/07/03 と約一年半ぶり。 電車内ではコードは書けないので、とりあえず問題文を熟読するところから始める。

占いのコストが明らかに安いので、基本的に掘らずに占いのみで油田の場所を特定したいなという感覚はあった。 一方で、同時に全部のマスを特定しないと全く点数が貰えない形式なので初期方針の決定まで結構迷った。 素直には盤面全体を 3x3 とか 4x4 ぐらいのグリッドに区切って順番に占って、おおまかな配置を推定できるんじゃないかと考えたが、1 マス単位で配置を特定できる方針になかなか結びつかない。

そのうちに思いついたのが、縦方向の位置と横方向の位置を独立に解くという方針だった。 2 次元的な配置を解くのはパターン数が多くて大変なので、1 次元化して解いてからそれらを組み合わせたほうが解きやすいのではないかと考えた。

縦横独立に解けることの説明
Polyomino を横方向に動かしても、縦方向の射影は変化しないので、縦横独立に解ける

とりあえず 各行,列の埋蔵量の分布が必要なのでこれを推定するために最初にすべての行と列を一通り占う。 この時点でのコストは盤面サイズ N について (1/sqrt(N)) * 2N = 2 * sqrt(N) なので、だいたい 6 から 9 マスぐらい掘るぐらいに相当する。 これで精度の良い予測が得られるのなら、石油が埋まっているマスを全部掘るよりは大幅に安いコストで済みそうである。

一次元方向の解の生成については、全列挙した場合のパターン数が N^MN は最大でも 20 なので M も 3 ぐらいまでなら全列挙が効きそうである。 しかし、M が大きくなると全探索は厳しいのでその場合はビームサーチ化するのが良さそうに思った。 探索途中も含めた評価値としては、全部配置した時点で (予測値) - (仮説) ができるだけ 0 に近づいて欲しいので、マイナスになった行にペナルティをつけていくことにした。 観測誤差の分布からして、あまり真の値からかけ離れたずれは発生しづらいはずである。 そのため、マイナスの絶対値が大きくなった場合は線形よりペナルティを大きくしたいと考え、マイナスになった行の絶対値の 2乗和をペナルティに使ってみることにした。

もしこの探索で正しい解が得られなければ、予測精度を上げるためにもう一周行と列の占いを行い、更新された予測をもとに再び同じように探索を繰り返す。

2/12 (日) までには一通り動く実装が完成し、1 回目の提出を完了した。 M が大きいケースはとりあえず全マス掘る bruteforce のまま。

中盤

ひと通り動くようになった実装について、M = 3 程度の小さい問題でもうまく解けていないものがあることがわかった。 Visualizer を使って観察してみると、eps の大きいケースでは観測の精度が悪く、全くはずれているわけではないものの真の配置から数マスずれたような解答になってしまっている場合があることがわかった。

予測が間違っていそうな行および列の精度を上げるために、これらの行, 列だけ占いの回数を上げるようなことはできないかと考えていた。 上位の解候補で値が異なる場合が多いような行, 列が観測精度を上げる価値が高いのではないかと思い、上位の解候補の集合について分散が大きいような行, 列を重点的に占うといった実装を試してみたが、思ったように点数を伸ばすことができなかった。 予測が間違っている行, 列を特定するということ自体が難しい上、仮にそれを特定できてもそもそも eps が大きいようなケースでは繰り返し観測したところで大して予測が改善しないという問題もあったのではないかと思う。

このようなケースでは占いだけで特定するのは諦め、有望そうな部分を掘る実装に切り替える方針を固めた。 とりあえず分散が大きい行, 列の占いを繰り返す代わりに 1行分まとめて掘ってしまうことにした。 これなら一気に解の候補が狭まるので、何行か掘ればほとんど必ず正しい解答に辿りつける。

一方、M が小さいケースですらこんなに苦労しているのだから、M が大きいケースは占いだけで解答するのはかなり厳しいだろうと判断し、それなりの数掘ってもいいので bruteforce よりはましな実装を考えることにした。 基本的に、1マス油田を見つければ幅優先探索で広げていけば隣接する油田は全て見つけられる。 最初の1マスを見つけるのに工夫がいるが、ここは占いでやることにした。 具体的には、最初は盤面を N*N/2 マスずつに2分割してそれぞれ占い、埋蔵量が多そうな方を選ぶ。 選んだほうの集合をまた 2分割してそれぞれ占い、また埋蔵量が多そうな方を選ぶ... という感じで占い範囲をだんだん縮めていく。 M が大きいケースは明らかに難しそうだし、テストケースの生成方法から M が大きくなるケースは少ないことがわかっていたので、あまり深追いせずとりあえずこれで完成とみなすことにした。

2/15 (木) まででこのあたりの実装を一通り終え、再び提出

終盤

観測誤差により正解を見つけられない問題について 1 行まとめて掘る方法で対処していたが、それではさすがにコストが高すぎるだろうと思って、なんとか掘るマス数を減らせないか考えていた。 思い付いたのは、上位の解候補で盤面の状態を比較し、予測が食い違うマスだけをピンポイントで掘って解を絞りこむというやり方である。

配置の差分についての説明
縦に 1 マスずれた配置のどちらであるか特定するには 4 箇所のどれかひとつを掘れば十分

例えば 100 通りの解候補を比較して、あるマスについて

  • 40 通りは 0
  • 30 通りは 1
  • 30 通りは 2

であったとすれば、そのマスを掘ることで悪くとも 40 通りまで解が絞りこめる。 このやり方はそこそこうまくいってスコアを伸ばすことができた。

残る問題は結局最初の行/列の予測精度によって解候補の質が大きく影響されてしまい、特に油田の数が増えてくると解候補の数も増えてくるため真の解が解候補から外れてしまうことであった。 探索のビーム幅を上げると時間が足りないし、前述のように占い回数を増やしてみても思うように予測精度は上がらない。 また、解候補が油田の探索順に依存していたので、探索順序をシャッフルするといったこともしてみたが、効果があったかどうかは微妙。

ある程度有望かもしれないと思ったアイデアとして、一気に全部の油田の配置を求めようとするのではなく油田をいくつかのグループに分けてそれぞれ解いたあとで組み合わせるというものを考えていた。 例えば、M=6 の問題であれば油田 3 つのグループを 2 つつくり、それぞれ独立に解いてから解を組み合わせるという具合である。 こちらも実装してみたのだが、グループが大きいと思ったよりもうまく解が見つからず、グループが小さすぎる (例えばそれぞれの油田 1 つずつを全部独立なグループとみなす) と一応解けるが、候補の絞りこみのために掘るマスが多すぎて BFS 解法と大差無いスコアになってしまっていた。 調整次第では使えそうな感覚はあったのだが、実装し終わったのが結構ぎりぎりなこともあってこのアイデアについては提出は断念した。

終了後

上位陣と比べるとスコアに歴然とした差があるのは明らかだったので、どういう解法を使っているのかについて興味があった。 終了後に writer の wata さんや参加者トップの terry_u16 さんが簡単な解説を出してくださっていたので早速読んでいた。 どうやら、問題をきちんと定式化し直すと占いの結果から事後確率が最大となるような配置を見つけ出すベイズ推定の問題と見なすことができて、どのような占い範囲を採用すべきかの評価については、その占いから得られる相互情報量として定量化できるということらしい。

こういう統計的なアプローチが全く考察の範疇に入れられなかったことは悔しいが、非常に面白そうな解法でもある。 エントロピーとか相互情報量とか、大学の講義で昔習ってはいたものの、どうやって応用できるのかいまいち理解できていない部分があった。 AHC の問題を通してこういった理論の応用例に触れることができるというのもとても面白い体験だと思う。 次にこの種の問題が出たときには考察のレパートリーに加えられるように、延長戦でいろいろ試してみたいと思っている。

SATySFi でお絵描きライブラリをつくる構想

モチベーション

スライドなどで使うような、簡単な図形を組み合せた説明図を手軽に描きたい。 Drawio のような GUI で作図するツールもあるが、テキストベースで描けると再利用しやすく管理上も便利であったりする。

TeX には PGF/Tikz という図を描くためのライブラリがある。 Tikz はかなりつくりこまれていて、いろいろな図が描けるしドキュメントもかなり徹底して書かれている。 しかし、Tikz の中身は TeX 芸の結晶であり、その副作用として少しでも書き方を誤るとたちまち解釈不能なエラーメッセージに悩まされることになる。

そこで、型検査のできる SATySFi で Tikz ライクなお絵描きライブラリをつくりたい。 SATySFi も pdf の描画命令を直接埋めこむようなプリミティブを備えているから、原理的には Tikz などと同じようにお絵描きをつくることができるはずである (一部サポートしていない pdf のプリミティブはありそうだが)。

「Tikz ライク」ってなんだ

上で「Tikz ライク」に図を描きたいといったが、これを具体化して考えたい。 考えた範囲で次のようなものが思いついた。

  • node というオブジェクト
  • style による描画方法のカスタマイズ
  • 交点計算・接線計算などの幾何計算
  • パスの変形
  • let 等による変数定義や pgffor による繰り返しなどによる構造化

node というオブジェクト

Tikz では、

  • 中身のテキスト
  • 枠線

をまとめて扱う node というオブジェクトを基本単位にして描画を行うことが多い。 基本的に図の中にあらわれるオブジェクトは node としてつくり、node どうしを相対的に配置したり枠線の形・大きさを変えたりすることで様々な図をつくることができる。

また、node 間を edge で結ぶ際には自動的に edge と node 自身の枠線が被らないように、edge のうち枠線の内側に入る部分を削って短くする処理が自動的に行われる。 この処理によって、エッジがノードの中に食いこむのではなく、枠線に触れるような形で edge を描くことができる。 地味な部分ではあるが、手軽に図を描くためには必須であると感じる。

node のようなオブジェクトベースでお絵描きを行える SATySFi のライブラリは自分の知る限りではない。

style による描画方法のカスタマイズ

Tikz では、線の太さや色・枠線の形などを style として指定することで描画されたときの見た目を変化させる機構が備わっている。 デフォルトで豊富な種類の設定項目が用意されているほか、これらを組み合わせて style を定義することもできて、この仕組みがある。 これらの仕組みによって同じようなオブジェクトに統一した style を適用したり、style の一部分の値を変更して簡単に見た目を調節したりすることができる。

Tikz の style のような方法で見た目の調節ができるような SATySFi のお絵描きライブラリは自分の知る限りではない。

交点計算・接線計算

Tikz では intersections というライブラリを使って交点計算をしたり、円の接線を簡単に描画したりできる機能がある。特に交点の取得は複雑な図を描く上でかなり重要な機能といえる。

交点の計算に関しては、yasuo-ozu さんによる xpath という実装がある。 xpath は交点の計算の他にもパスの長さの取得やパスの途中分割など便利な機能を備えている。

パスの変形

Tikz では decoration という機能によって、パスの変形を行うことができる。 例えば、例えば直線や円をつくってからギザギザの線に変更したりするといったことが可能で、これによって複雑な線や図形の描画が可能になる。

SATySFi では xpath で数式的な変換や法線方向に曲線全体をずらすような移動が xpath で実装されている。 また、複雑な線を描くライブラリとしては monaqa さんによる railway というライブラリもある。 こちらは部分的なパスをつないだり繰り返したりするのを描きやすくするほか、パス全体のスケール変換などが行えるようである。

変数定義、繰り返しなどによる構造化

Tikz では TeX のマクロを駆使して独自に let 文のような変数定義構文を用意しているほか、付属の pgffor ライブラリによる for 文が書けたりするなど汎用のプログラミング言語にみられるような構造化のための機構が用意されている。 これらは、"TeX の上で使える" という点においては特殊であるが、SATySFi であればそもそも言語として一般的な関数型言語相当の構文が使えるので、これらの構文を自分でつくる必要はない。 むしろ、この点においては言語組み込みの豊富な機能が使いやすい SATySFi のほうが Tikz よりだいぶ書き味はよくなるはずである。

目標

これまで見てきた Tikz の特徴のうち特に現状の SATySFi のエコシステムに足りていないのは、node というオブジェクトベースでのお絵描きや、組み合わせ可能な style による見た目の調整のための機構ではないかと思う。 最終的にはこれらを使いやすい形で提供するようなライブラリをつくりたい。 今回はまだかなり荒削りであるが、部分的にこれを実現するような構成を模索してみた。

現状 SATySFi 上でもっとも機能が充実したパス操作ライブラリは xpath であると思われるので、これをベースに実装する。

現状

https://gist.github.com/sankantsu/3ca29d9daa18ee6c4818c2f3285e4622

今日急いで書いたのでまだライブラリの形にすらなっていないが、上の gist に現状動作する実装がある。 組版した結果は次のようになる。

大枠のアイデア

現状の型宣言を抜き出したものを以下にのせる。 node は、

  • 参照点 (position)
  • 枠線を書く関数 (border)
  • テキストなどの中身 (content)

描画する際の線の太さ・色などのスタイルは context を通して渡されるものとする。 node 単体へのローカルなスタイルの変更は context を変換する関数などをもたせることによって実現できそうである (未実装)。

edgenode 間を結ぶ線を表す。

picture は複数の nodeedge をまとめたひとつの"絵"である。

% type declarations
type context = (|
  line-width : length;
  draw-color : color;
  inner-sep : length;
  precision : length; % precision for calculating intersections
|)

type node = Node of (|
  position : point;
  border : context -> point -> XPath.t;
  content: graphics;
  % style: ctx -> ctx
|)

type edge = Edge of (|
  from: node;
  to: node;
  shorten: point * point;
  % style: ctx -> ctx
|)

type picture = (|
  nodes: node list;
  edges: edge list;
  context: context;
|)

インターフェースの概観

\picture というインラインコマンドで picture を inline graphics に変換する。 使用例を以下に示す。

まず、start-picture ctx に適当な描画コンテキストを渡して初期化する。 ここに make-text-node text-ctxt point it などでつくった nodemake-edge from to などでつくった edge を追加していく。 ここで、text-ctx というのは SATySFi の通常の意味でのテキスト処理文脈である。

\picture(
    let text-ctx = get-initial-context 100000cm (command \math) in
    let node1 = make-text-node text-ctx (0pt, 0pt) {This is a test} in
    let node2 = make-text-node text-ctx (100pt, 30pt) {fgh} in
    let edge1 = make-edge node1 node2 in
    start-picture default-context
    |> add-node node1
    |> add-node node2
    |> add-edge edge1
);

今後

見ての通りこの構想はまだまだ未完成なところが多い。 以下にいくつか今後の課題を挙げる。

  • node の anchor (north, east など)
  • style の適用方法の整理
  • edge の先端を矢印にできるようにする
  • 円などを用いた場合に交点計算の結果がおかしい

node の形を円にしたときの描画結果が以下。 明らかに交点ではない場所に交点が計算されてしまって edge の長さがおかしくなっているように見える。 これに関しては xpath の交点計算部分のデバッグが必要そうな気がしている。

また、SATySFi 自体の部分として、図の中にテキストを埋め込みたい場合に本文と同じテキスト処理文脈をもってきたりできないかというのを思った。 現状、get-initial-context で新しい context をつくっているので本文と全然違うスタイルになってしまう可能性がある。

最後に、もしこんなのがあったら面白いとか、こういうインターフェースにしたほうが良いとか、気軽に意見やコメントをもらえたら嬉しいです。