リアルタイム受験した。3時間55分で全完。
L - マンションの改築
差分di:=ai−ai+1に注目すると、di=0になるiを数えればよいことがわかる。クエリによる(di)の変動は以下のようになる:
- タイプ1のクエリ: 奇数のiについてdi+=v、偶数のiについてdi−=v。
- タイプ2のクエリ: 奇数のiについてdi−=v、偶数のiについてdi+=v。
- タイプ3のクエリ: du−1−=v、du+=v。
di=0になるiを数えたいので、さらにtable[x]:=di=xであるiの総数、という情報をハッシュテーブルなどで保持しておくとよさそうだ。しかし、(di)やtableをクエリ毎に愚直に操作すると、タイプ1と2については間に合わない。
偶奇にわければタイプ1, 2はまとめて処理できそうなので、offset奇,offset偶という量を保持することにする:
- タイプ1のクエリ: offset奇+=v、offset偶−=v。
- タイプ2のクエリ: offset奇−=v、offset偶+=v。
diの真の値は、これらのオフセットを足したものということになる。さらに、table奇[x]:=di=xであるような奇数iの総数とし、table偶も同様に定義して、タイプ3のクエリでこれらを更新する:
- タイプ3のクエリ: du−1−=v、du+=v。さらにdu−1とduの変更に応じて、table奇,table偶を適切に更新する。
こうすると、各クエリを処理したあとの答えはtable偶[−offset偶]+table奇[−offset奇]で求まることになる。
M - 筆塗り
与えられたグラフTを根付き木とみなす。i回目でパスu−vを塗るとしたとき、uから塗りながら木を上昇していってLCA(u,v)の深さ以下になったらそこで塗りは無効になると考えることができて、vについても同様である。そこで、事前処理でq[v]:=一方の端がvであるすべてのクエリ番号(のリストや可変長配列)、という情報を用意しておくことにする。
そして、Tを葉からボトムアップに処理する。頂点vを処理する際に「子からvに上ってきた時点でまだ有効かもしれないクエリ番号」を番号の降順で優先度付きキューに保持する。具体的に手順を書くと、
- vの子c1,c2,...,ckの保持する優先度付きキューQc1,...,Qckとq[v]をすべてマージしたキューQvを作る。
- Qvからクエリ番号iを取り出し、iに対応するLCA(ui,vi)の深さを調べ、vの深さがそれ以下であればiを捨てることを繰り返す。vのほうが深ければストップし、vとその親をつなぐ辺がクエリiで塗られていることを確定する。(Qvが空になるまで見つからなければこの辺は一度も塗られていないことになる)
ダブリングでLCAを求めてマージ可能なヒープを使った場合、計算量はO((N+Q)(logN+logQ))となる。コンテスト中にはpairing heapを使ったが、二分ヒープでもマージテクをすればlogQが掛かるだけなので十分間に合う。
操作を逆から見ると簡単になるのは確かに……
O - 宝箱
一読して、セグメント木上でin-placeにDPする方法がありそうに見えたので、うまい更新方法を探した。
以下、0-basedで考えて、鍵屋iに対応する区間を半開区間[li,ri)で持つことにする。とりあえず愚直なDPを考えると、dp[x]:= 右端(つまりri)がx以下の鍵屋のみを使って得られる最大利益、とするのがよさそう。右端がxと一致する鍵屋iがただひとつ存在するケースを例にとると、遷移は
dp[x]←max(dp[x−1],dp[li]+ali+ali+1+...+ari−1,dp[li+1]+ali+1+...+ari−1,...dp[ri−1]+ari−1)
となる。遷移を見ると、aiを加算している部分が前もってdpに足されていれば、区間maxでまとめて処理できそうだ。実際、dp[x]を処理したあとの遷移の式には、dp[0],dp[1],...,dp[x]は常にaxが加算された形しか出てこないので、dpを遅延セグメント木などで持っておいて、必要なタイミングで区間加算すればよい。疑似コードで書くと以下のようになる:
res = 0
for r from 1 to N:
dp[r] = max(dp[r], dp[r-1])
dp[0], ..., dp[r-1] += a[r-1] # 区間加算
for l, c in 右端がrの鍵屋:
dp[r] = max(dp[r], max(dp[l], ..., dp[r-1]) - c)
res = max(res, dp[r])
上のdpの定義はdp[x]を処理した瞬間にしか成り立っていないことに注意。