/ 最近 .rdf 追記 編集 設定 本棚

脳log[20231219] JOI 2023/2024 二次予選 過去問



2023年12月19日 (火)

最終更新: 2024-01-18T16:02+0900

[AtCoder] JOI 2023/2024 二次予選 過去問

AtCoder の問題ではないが精進。ABCE の4問。

 A 問題 カードゲーム 2 (Card Game 2)

基本的にはカードを順番にスキャンして判断をする。ハッシュ表を使って自分が一番小さいカードである場合、2番目、3番目の場合の3パターンについてカードが3枚揃っているかを確かめる。もしくは、昇順にソートして自分が一番大きいカードであるケースについてカードが揃っているかを確かめる。

 提出 #48433809 (AC / 121 Byte / 125 ms)

 B 問題 買い物 2 (Shopping 2)

N 個の商品がありそれぞれの商品は M 種類の商品カテゴリのどれかに属している。M 日間のセールがあり、セール期間のある一日にはあるひとつのカテゴリの商品が半額になる。Q 人の客がそれぞれある日に訪れ、ある範囲の連続する商品を購入する。さていくら? Q 個答えよ。

割引がなければ範囲の総和を知るのに累積和を引くだけ。しかしある日にはあるカテゴリの商品が半額になっているので、範囲内にそのカテゴリの商品がいくつあるか知りたい。カテゴリごとに位置を昇順に記録して二分探索をした。

カテゴリごとに累積和を用意しようとすると最悪の場合長さ N の配列が M 個になって、N×M は大きすぎる。更新のあったところだけ記録しようとすると、さっき書いた「カテゴリごとに位置を昇順に記録」する方法になる。

 提出 #48434188 (AC / 397 Byte / 782 ms)

 C 問題 白色光 2 (White Light 2)

難しいね。いっぱい間違えた。制限時間が1秒とやや厳しめ。

3の倍数の長さ(0,3,6,9,...)の範囲を切り取ってみれば、左端の位置と右端の位置、それと RGBRGB...RGB 文字列との不一致の数からコストがわかる。不一致を単位時間で数えるためには3種類の累積和を用意しておけばいい。すなわち、RGB...RGB..BRG...BRG..GBR...GBR.. 文字列と S との不一致を数えた3種類。

ところで、N の上限が 20 万のときに範囲の両端を自由に選ぶと 20 万の2乗(割る2くらい)で間に合いませんね。範囲の左端を総当たりすることにして最善の右端をどうやって決めるかというところで3回 WA を出した。

最初の提出 #48448208 では左端を右に移動するごとに右端を左右に移動させてみる尺取りをした。特定の制約を追加したサブタスクはクリアしたが、N の制約を甘くしただけのサブタスクで一定割合の WA がある。時間は間に合っていて(局地的に)最適な答えを見つけることもできているが、最善の答えをときどき見逃しているということ。

次の提出 #48453131 は不等号にイコールを追加するような微修正で、3割くらい WA の数は減ったけど本質的な誤りは修正されていない。

3番目の提出 #48453143 は3つのサブタスクで2つずつ WA があるという結果で、ほぼ AC と同じ。実はこれまでの2つの提出ではケアしていた、左右どちらかから全ての文字列を削除するケースに対応したコードを削った結果 WA を生んでしまっている。正しい解法を見つけたときにもういらない気がしたんだよなあ。

 提出 #48453322 (AC / 481 Byte / 224 ms)

これが AC。右端を見つけるのにスライド最小値(?)だと思うものを使った。実装はしなかったけど三分探索も検討していた。知っている解法を総当たりして正当性の判断をジャッジに委ねるのはやめようね。

 E 問題 高速道路の通行料金 (Highway Tolls)

時間に依存したコストをどう扱うか。t=0 となる位置を1から N へのパスの中のどこに置くかでコストが変わる。パスの外に置いても得をしないので考えない。どこに置くのが最善で、どうすれば最善が見つけられるか。

t=0 となる地点をあるパスの中のある辺に置くとする。1がある方のパスに A 個の頂点が、N がある方のパスに B 個の頂点があるとして、A<B なら t=0 の地点を辺上で N の方に近づけるのが良い。A=B なら辺上のどこにあっても同じ。なので、t=0 の地点をどこかの頂点から選んで良い。

t=0 となる頂点を決め打って、1からの最小コストと、N までの最小コストが求まれば良い。1からは順方向に探索し、N からは逆方向に探索すれば、2回の手間で答えが求まる。N 頂点全てを始点にして N 回の探索をすることは時間的に許されない。

 提出 #48650001 (AC / 635 Byte / 113 ms)

提出日の開きを見るとわかるけど、この AC はほぼ一週間がかりだった。

探索は BFS。移動コストが固定値の C と、パスに含まれる頂点数に比例する L×K なので、BFS によって頂点数が単調増加だと想定できると、単純にコストの総和を比較するだけで最短経路が見つけられるし、頂点数に比例したコスト計算もやりやすい。

サンプルの4、5、6あたりの答えがいつまでも合わなくて、ああでもないこうでもないと試行錯誤していた。さっぱり見えていなかったのは AC 提出の 22 行目、頂点 N までのコストを探索する D 関数に与える初期値(DN = D[ヨ[N].group_by{|s,|s}.map{|s,stcs| [s,stcs.map{_3}.min] }.to_h,ヨ])。1からのコストを求める 21 行目(D1 = D[{1=>0},E])と比較すると長さだけ見ても段違いなのがわかる。この違いがさささっと把握できる人はすごすぎると思います。

N から逆向きにコストを計算するのに N の隣接頂点を始点にすればいいとはなかなか……全然わからない。そこにやっと気がついても N とその隣接頂点 V を端点とする辺が複数あるということがまた思い出せない。

たいへんだった。これ予選なんですって。

今3ページくらい E 問題の AC 提出があるんだけど、Ruby の 113 ms が最速だった。おもしろ。ただ、前回の言語アップデートから AtCoder のジャッジは、言語ごとに最初の1ケースだけウォームアップ時間が計測に含まれるみたいな雰囲気があるので(このときの話の関連>20230807)、ワースト1ケースのタイムが特異例である場合の比較に意味はない。

こちらの提出 #48433529 を見ると N×M のループを順方向と逆方向の2回回してるだけに見える。辺を端点ごとにまとめたりもしていない。どういうことなの? 結局 L[j]*K(i+1) を掛けるか i を掛けるかというだけの違いだったの? そうみたい。

 提出 #48653380 (AC / 566 Byte / 108 ms)

頂点数を1減らしてコスト計算する試みはけっこう初期にやっていて、でもたぶん他の部分のバグのせいで答えが合うところまでいかなかった。AC コードがある今同じ方針でやってみるとふつーに、よりシンプルに、答えが出た。なんだよもー。さっき書いた 21、22 行目だけど、こうなった。

D1 = D[1,1,E]
DN = D[N,0,ヨ]

そう、これくらい単純に順方向と逆方向の探索ができると思っていたし、実際できるのに、どうして1週間も沼にはまりこむ結果になったのか、謎だ……。