2020年11月8日日曜日

第四回 アルゴリズム実技検定

リアルタイム受験した。3時間55分で全完。

L - マンションの改築

差分di:=aiai+1d_i := a_i - a_{i+1}に注目すると、di=0d_i = 0になるiiを数えればよいことがわかる。クエリによる(di)(d_i)の変動は以下のようになる:

  • タイプ1のクエリ: 奇数のiiについてdi+=vd_i \mathrel{{+}{=}} v、偶数のiiについてdi=vd_i \mathrel{{-}{=}} v
  • タイプ2のクエリ: 奇数のiiについてdi=vd_i \mathrel{{-}{=}} v、偶数のiiについてdi+=vd_i \mathrel{{+}{=}} v
  • タイプ3のクエリ: du1=vd_{u-1} \mathrel{{-}{=}}vdu+=vd_{u} \mathrel{{+}{=}} v

di=0d_i = 0になるiiを数えたいので、さらにtable[x]:=di=x\operatorname{table}[x] := d_i=xであるiiの総数、という情報をハッシュテーブルなどで保持しておくとよさそうだ。しかし、(di)(d_i)table\operatorname{table}をクエリ毎に愚直に操作すると、タイプ1と2については間に合わない。

偶奇にわければタイプ1, 2はまとめて処理できそうなので、offset,offset\operatorname{offset}_奇, \operatorname{offset}_偶という量を保持することにする:

  • タイプ1のクエリ: offset+=v\operatorname{offset}_奇 \mathrel{{+}{=}} voffset=v\operatorname{offset}_偶 \mathrel{{-}{=}} v
  • タイプ2のクエリ: offset=v\operatorname{offset}_奇 \mathrel{{-}{=}} voffset+=v\operatorname{offset}_偶 \mathrel{{+}{=}} v

did_iの真の値は、これらのオフセットを足したものということになる。さらに、table[x]:=di=x\operatorname{table}_奇[x] := d_i=xであるような奇数iiの総数とし、table\operatorname{table}_偶も同様に定義して、タイプ3のクエリでこれらを更新する:

  • タイプ3のクエリ: du1=vd_{u-1} \mathrel{{-}{=}}vdu+=vd_{u} \mathrel{{+}{=}} v。さらにdu1d_{u-1}dud_uの変更に応じて、table,table\operatorname{table}_奇, \operatorname{table}_偶を適切に更新する。

こうすると、各クエリを処理したあとの答えはtable[offset]+table[offset]\operatorname{table}_偶[- \operatorname{offset}_偶] + \operatorname{table}_奇[- \operatorname{offset}_奇]で求まることになる。

M - 筆塗り

与えられたグラフTTを根付き木とみなす。ii回目でパスuvu-vを塗るとしたとき、uuから塗りながら木を上昇していってLCA(u,v)\operatorname{LCA}(u, v)の深さ以下になったらそこで塗りは無効になると考えることができて、vvについても同様である。そこで、事前処理でq[v]:=\operatorname{q}[v] :=一方の端がvvであるすべてのクエリ番号(のリストや可変長配列)、という情報を用意しておくことにする。

そして、TTを葉からボトムアップに処理する。頂点vvを処理する際に「子からvvに上ってきた時点でまだ有効かもしれないクエリ番号」を番号の降順で優先度付きキューに保持する。具体的に手順を書くと、

  1. vvの子c1,c2,...,ckc_1, c_2, ..., c_kの保持する優先度付きキューQc1,...,QckQ_{c_1}, ..., Q_{c_k}q[v]\operatorname{q}[v]をすべてマージしたキューQvQ_vを作る。
  2. QvQ_vからクエリ番号iiを取り出し、iiに対応するLCA(ui,vi)\operatorname{LCA}(u_i, v_i)の深さを調べ、vvの深さがそれ以下であればiiを捨てることを繰り返す。vvのほうが深ければストップし、vvとその親をつなぐ辺がクエリiiで塗られていることを確定する。(QvQ_vが空になるまで見つからなければこの辺は一度も塗られていないことになる)

ダブリングでLCAを求めてマージ可能なヒープを使った場合、計算量はO((N+Q)(logN+logQ)){O}((N+Q) (\log N + \log Q))となる。コンテスト中にはpairing heapを使ったが、二分ヒープでもマージテクをすればlogQ\log Qが掛かるだけなので十分間に合う。

操作を逆から見ると簡単になるのは確かに……

O - 宝箱

一読して、セグメント木上でin-placeにDPする方法がありそうに見えたので、うまい更新方法を探した。

以下、0-basedで考えて、鍵屋iiに対応する区間を半開区間[li,ri)[l_i, r_i)で持つことにする。とりあえず愚直なDPを考えると、dp[x]:=\operatorname{dp}[x] := 右端(つまりrir_i)がxx以下の鍵屋のみを使って得られる最大利益、とするのがよさそう。右端がxxと一致する鍵屋iiがただひとつ存在するケースを例にとると、遷移は

dp[x]max(dp[x1],dp[li]+ali+ali+1+...+ari1,dp[li+1]+ali+1+...+ari1,...dp[ri1]+ari1) \begin{aligned} \operatorname{dp}[x] \leftarrow \max( & \operatorname{dp}[x-1], \\ & \operatorname{dp}[l_i] + a_{l_i} + a_{l_i + 1} + ... + a_{r_i -1}, \\ & \operatorname{dp}[l_i+1] + a_{l_i+1} + ... + a_{r_i -1}, \\ & ... \\ & \operatorname{dp}[r_i - 1] + a_{r_i - 1}) \\ \end{aligned}

となる。遷移を見ると、aia_iを加算している部分が前もってdp\operatorname{dp}に足されていれば、区間max\maxでまとめて処理できそうだ。実際、dp[x]\operatorname{dp}[x]を処理したあとの遷移の式には、dp[0],dp[1],...,dp[x]\operatorname{dp}[0], \operatorname{dp}[1], ..., \operatorname{dp}[x]は常にaxa_xが加算された形しか出てこないので、dp\operatorname{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\operatorname{dp}の定義はdp[x]\operatorname{dp}[x]を処理した瞬間にしか成り立っていないことに注意。