PAST202004-OPEN

早めのバチャ。関連ツイートはけっこう見かけたけど、こういう抽象的なネタバレってどのくらい影響するんだろ。M以外を解いて94点。

A - エレベーター/Elevator

Aからクソムズ(まあABCとは違うので)。最初の文字が'B'かどうかで分岐して、1階が0となるように文字列を数値化する。あとは差の絶対値。

B - 多数決 / Plurality Voting

これは数えるだけなので簡単。今回は、配列を256まで用意して max_element(p, p + 256) - p をcharにキャストして出力した。

C - 山崩し/Landslide

読むのが大変だが、下から逆三角形に広げていくやつ。素直にシミュレーション。更新対象は'#'だけなので、横方向の範囲は1から98までと大雑把でいい。

D - パターンマッチ / String Match

えらく難しそうだが、かなり小さい制約なので全探索できる。英小文字に'.'を加えて27+27^2+27^3。計算しやすいように'.'ではなく'{'('z'+1)でやる。Sの探索範囲に気をつけて。Sの中で1箇所でもマッチしたら答えをインクリメントしてそのTの探索をやめる(ここけっこう混乱した)。

E - 順列 / Permutation

サンプルを見て何かサイクルに分解するやつかと思ったけどNが異様に小さいので素直にやるだけ。

(追記)必ず元の数に戻ってくる証明がわからなくて焦って、結局問題文を信じてそのまま提出してしまった。元の数を含まないループにはまるためには、そのループに含まれるある数への移動元が2つないといけないけど順列なので1つしかないね。

F - タスクの消化/Tasking

よくあるやつだけど難しいよね。まず、kの値によって1日目2日目あたりの戦略が変わるんじゃないかという心配が出る。ただ、実行できるタスクは増える一方なので大丈夫そう。実行解禁される順にソートして、日毎に実行できるものをpriority_queueにつめていって、貪欲にポイントの高いものをやっていく。

(解説PDFを見て)バケットソートでもいいのか。Bが小さいことを利用する方法もあるんだね。こういうの思いつきたいけど、結局実装としてはソート+優先度付きキューってことになりそう。

G - ストリング・クエリ/String Query

queueを使ってガシガシやる。削除して半端が出ないケースを慎重に。

H - 1-9 Grid

この制約ならダイクストラ法で問題ない。数字が1から9の9個で、ステージがいくつあるかがなかなかややこしかった。最初が0、1を踏んだら1、9を踏んだら9ということで10枚だ。0枚目のスタートから9枚目のゴールまでの最短距離がわかればいい。グリッド上の辺を列挙するのは何回かやって慣れてきたけど、向きによってどこの数字を見るか変わるしさすがに少し時間がかかった。

I - トーナメント / Elimination

難しそうだけど、単にトーナメントで何回戦うか答えるだけ。それもわりと難しいが、少し考えていい実装を思いついたので書き進める。vectorで人の番号と強さを持ち、p[i] = (p[i * 2]とp[i * 2 + 1]の勝者) とする。勝った側の勝数をインクリメントしていく。繰り返し毎に人数は半分になるので、計算量にlogは付かない。何回勝つかを求めて、優勝者以外に1を足してもいいのでそうした。

J - 文字列解析/Parse

(最終的にも)1000文字以下なので、1個ずつ括弧を外していけばいい。最後に見た'('の場所を記録しておき、')'が来たらそれとペアにする(処理したらループを抜けてまた最初からペアを探す)。ペアの間には'('も')'もないことが保証できる(ちょっと不安だったが、どちらもないんだからしょうがない)。substrとか使って雑に連結して間に合う。括弧がなくなったら出力。

ここまで、考察または実装が軽い問題も多く、かなりいいペースで解けている。

K - 括弧/Parentheses

10分くらい考えて何もわからんので次の問題へ。'('を+1、')'を-1として途中負にならず最終的に0ってことだよね。どうやってそれを実現するか。2種類の操作を別々に考えてくっつける?O(N^2)は間に合うし。でも削除だけでもわからん。難しすぎる。

Oまで通して、残ったKとMのうち簡単そうなKをやる。DPでできるやんけ。しかも2種類の操作は一緒にやるとしても同じことだ。DPに気づいた瞬間こんなに簡単だとは。ただし実装までイージーではない。INFで初期化すべきところをちゃんと初期化する。+1はN/2を超えてしまうと0に戻れなくなるので ll dp[3001][1501]; くらいでいいかと思ったけど、実装のしやすさから ll dp[3001][3001]; にした。N/2が整数とは限らんし。dp[i][n]をINFにして、あと特別扱いが必要なのはdp[i][0]だけ。そこは書く。最後はdp[n][0]を出力するだけなので楽。

L - 辞書順最小/Lexicographically Minimum

判定は簡単なのにわざわざ不可能な入力もありになっている。判定を書くことでスムーズに考察できるという誘導だろうか。最初の数は可能な最小のもの、最小となる数がたくさんあっても番号が若いものを取って損しない。次以降の数も同様なので、最小のものをどんどん取っていけばいい。セグ木にA_iとiのペアを乗せる。「A_iが同じならiが小さいものを選ぶ」という処理を書くのが面倒だったのでpair<int, int>にした(minをとるだけでいい)。(K-1)*D+1文字が必要で、それだけあれば数列は作れる。あとは区間をDずつ更新しながら出力していく。

問題が比較的軽量なのはここまでだった(前回と違って、最後の3問だけがはっきり重い)。

M - 食堂/Cafeteria

問題設定がヤバい。回数と時間がごちゃごちゃに混ざる。読むたびに誤解して趣旨を知ることができない。「前回の利用がL日前である場合のみ」のところ、L日前でないとき食べないのはわかるが、L日前なら必ず食べるとは言ってないように見えて混乱した。10分ちょっとして、ヤバいので次の問題へ。

他の問題を解き終わり、残った35分をつぎ込んで考える。制約が10^5なのでかなり頑張って計算量を落とさないといけない。まず、料理毎に(10^5個の)vectorを持ち、その料理が出される日を詰め込む。間に好みの料理以外を食べる回数を保存しておきたいのでpair<int, int>にする。使いやすいように、vectorの長さを1増やす(のちに2倍にした)。しっかり0-indexedに変換しておく。あとはクエリに答える(好みが被ってたり最初の利用日が違ったりヤバ杉)。答えるのは好みの料理を食べる回数だ。好みの料理でないものを食べた回数や料理を食べた回数ではない。まず二分探索で次に好みの料理が出る日を求める。そこまでに回数を使い切ったら0を出力。次に、1周期で何回食べるかで割り算してTを剰余で小さくしておく。最後に、二分探索であと何回好みの料理を食べれるか求める。好みのだけでいいので少しは楽。この最後の二分探索の細かいところを詰める時間がなくて、終了となった。

結局この方針でACできたが、時間はめちゃくちゃかかった。難しいというより、(バチャが終わってしまうと)考えたくないくらい複雑だ。なお想定はダブリングらしい。Twitterで見かけたダブリングってOじゃなかったんだね。

N - ビルの建設 / Building Construction

敷地は重なることもある。例えば3つ重なってたらその3つのコストの和を求めればいいわけだ。頼むから座標圧縮なしで解ける問題であってくれと祈りながら考えるが、(普通にやるならたぶん)必要だった。敷地とクエリを共通のstructに入れてx座標でソート。区間可算点取得をするためにBITを使う(いもす法のイメージ)。BITを使うために座標圧縮する。境界を含むので、x座標の方向ではソートのときに考慮し、y座標はBITの足す場所で考慮する。なぜかサンプルが通る(というくらい長い実装だった(途中で詰まったわけでもないのに))。方針は(典型なので)わりとすぐ見えたものの、やはり実装にかなりの時間をとられてしまった。

O - 可変全域木 / Variable Spanning Trees

なぜ頂点でなく辺なのだろうと思ったが、当然ながら全域木には全ての頂点が含まれるのだった。さて、まずは普通に最小全域木を求めてみる。そこに含まれる辺についてはこの全域木の重みが答え。問題はそれ以外だ。最初にその辺をつなげてからクラスカル法を適用するのとかを考えていたが、最小全域木に辺を付け足すというのでよかったんだ。木に辺を一つ追加すると閉路ができる。いわゆるなもりグラフ。その閉路から一つ辺を取り除いたものは木になるはず。つまり、閉路の中で重みが最大の辺がわかればいい!

ということでオイラーツアーとセグ木を貼ろうとする。グラフは、まず最小全域木を作ってその木で構築する。閉路は、元の木で考えるとLCAで合流する(面白い)。んで、オイラーツアーがあっても閉路の最大重みはわからないと気づく。マジかよ。LCAはわかるし、そこまでのパスもわかる、と思いきやオイラーツアーは寄り道するのでそのパスだけの最大値はわからない(普通にDFSとかででもできないか考えたけど何も出てこなかった)。困っていたが、突然ダブリングが降ってきた。LCAはダブリングで求めて、ついでに最大値もダブリングで求まるじゃないか!最小全域木の重みに今の辺の重みを足して、辺の両端の2つの頂点からLCAまでのパスに含まれる辺から最大の重みを求める。辺はコスト昇順でソートするので、辺番号が何の番号だか混乱した。

サンプルが合わない。ここで、最小全域木は一通りと限らないことに気づく。別の最小全域木なら、閉路にもっと違う辺が含まれていたのでは?これね、解法を証明してないというより、単にクラスカル法の証明を知らない(は?)。まあ、だとしてもこの単純なサンプルが通らないのはおかしいのでバグを探す。なんと、木を構築するときに、逆向きの辺を追加せず同じ向きの辺を2回追加していた。コピペしてそのままかよ。サンプルが合ったので提出して(雰囲気的にクラスカル法が正しいならこれも正しいやろみたいのはあった)AC。実装だけでなく、このデバッグに時間を大量消費させられたのは痛かった(もっとも、これだけ重めの問題を解いていたら1回くらいはこうなるか)。