2020年11月29日日曜日

CS Academy Round #76 - Pyramids

CS Academy Round #76 - Pyramids

ある種の区分線形関数に囲まれた領域の格子点を数える問題。区分線形関数を構成する線分の総数がO(N){\mathcal O}(N)なので、全部列挙して線分毎に下にある格子点を数えればよさそう。

まずは、与えられた点で折れ線を構成するものだけを左から列挙したい。点をccの昇順でソートしてスタックを持ちながら走査する。ある点(r,c)(r, c)による山を処理するとき、スタックの先頭に入っている点(=ひとつ前の山の頂上)による山との関係には次の3通りがある:

  1. 前の山に完全に含まれる
  2. 前の山を完全に含む
  3. 前の山と一部重なるか、遠く離れていて重ならない

今見ている点を(r,c)(r, c)、スタックの先頭に入っている点を(r,c)(r', c')として、

  • c+rc+rc+r \le c' + r'なら1、つまり前の山に含まれるので無視
  • crcrc-r \le c'-r'なら2、つまり前の山を含むのでスタックをポップしてさらに前の点を見る
  • それ以外は3なので新しい点をスタックにプッシュする

こうするとスタック(の反転)には山の頂点として有効な点だけが左から並ぶので、その間にできる谷に関する格子点をそれぞれ数えればよい。

数え上げパートについて。山の頂点が左から(r1,c1),...,(rk,ck)(r_1, c_1), ..., (r_k, c_k)と並んでいるとする。頂点(ri,ci)(r_i, c_i)(ri+1,ci+1)(r_{i+1}, c_{i+1})の間にできる谷は直線y=(xci)+riy=-(x-c_i)+r_iと直線y=xci+1+ri+1y=x-c_{i+1}+r_{i+1}の交点であり、そのyy座標は(ri+ri+1+cici+1)/2(r_i+r_{i+1}+c_i-c_{i+1})/2と表せる。そこで、H=max(0,(ri+ri+1+cici+1)/2)H= \max(0, \lceil (r_i+r_{i+1}+c_i-c_{i+1})/2 \rceil)としてまずは下部の長方形の格子点をH(ci+1ci+1)H(c_{i+1}-c_i+1)と数え、さらに上部の三角形の格子点を(riH)2+(ri+1H)2(r_i-H)^2 + (r_{i+1}-H)^2と数える。また、左端の頂点(r1,c1)(r_1, c_1)の山の左側半分にある格子点r12r_1^2個と右端の頂点(rk,ck)(r_k, c_k)の山の右側半分にある格子点rk2r_k^2個は別に加算する。このとき、各頂点の下にある格子点をちょうど2回ずつ数えているので、iri\sum_i r_iを引けば正しい数になる。

2020年11月27日金曜日

CS Academy Round #79 - Groups

計算量の説明がどこにも書かれていなくて困った。以下は自分の理解。

ii番目のクエリで与えられるPiP_i人の人間について、代表者の人数をxix_i、代表者以外の人数をyiy_iとする。xix_i個のグループそれぞれについて、走査して離脱しないメンバーが見つかったら打ち切るのが想定解らしい。D=105D=10^5として1つのクエリで調べるノード数は最大でmin(xiyi,D)\min(x_iy_i, D)なので、計算量はi=1Qmin(xiyi,D)\sum_{i=1}^Q \min(x_iy_i, D)と書ける。これを評価したい。

まず、i=1QPi=i=1Q(xi+yi)D\sum_{i=1}^Q P_i = \sum_{i=1}^Q (x_i + y_i) \le Dという制約がある。xi+yix_i + y_iが一定のときxiyix_iy_ixi=yix_i = y_iで最大値を取るので、i=1QxiD/2\sum_{i=1}^Q x_i \le D/2という条件下でi=1Qmin(xi2,D)\sum_{i=1}^Q \min(x_i^2, D)を評価する問題ということにいなる。面倒なので、代わりにi=1QxiD\sum_{i=1}^Q x_i \le Dとしておく。

xi+xjx_i + x_jが一定のとき、xi2+xj2x_i^2 + x_j^2xj=0x_j=0で最大になる[1]ので、i=1Qxi2\sum_{i=1}^Q x_i^2を大きくするには値をできるだけ偏らせるのが最善である。ただし、1つの変数をD\sqrt Dより大きくしても意味がないので、最大値を取る配分の一つは(x1,x2,...,xQ)=(D,...,D,0,...,0)(x_1, x_2, ..., x_Q) = (\sqrt D, ..., \sqrt D, 0, ..., 0)という形になる。i=1QxiD\sum_{i=1}^Q x_i \le Dより、最大でD\sqrt D個程度のxix_iD\sqrt Dにできるから、このときi=1Qmin(xi2,D)=DD\sum_{i=1}^Q \min(x_i^2, D) = D\sqrt Dと見積もれる。


  1. 平面上で、原点を中心とするマンハッタン距離に関する円と、ユークリッド距離に関する円を思い浮かべればよい。 ↩︎

2020年11月26日木曜日

ARC 108 F - Paint Tree

主張: 与えられた木TTの直径をDDとし、長さDDのあるパスの端点Vb,VwV_b, V_wが黒、白に塗られているとする。また、黒に塗られた頂点同士の最大距離をXXとし、距離XXの黒に塗られた2頂点A,BA, Bが与えられているとする。このとき、パスVbAV_b - AまたはVbBV_b-Bの少なくとも一方は長さXXである。

paint_tree

証明: パスVbA,VbBV_b - A, V_b-Bの両方の長さがXX未満と仮定する。3頂点Vb,A,BV_b, A, Bの間にできる3つのパスに共通して含まれるただ1つの頂点をccとすると[1]d(c,A)+d(c,B)=Xd(c, A) + d(c, B) = Xだから、d(c,A),d(c,B)d(c, A), d(c, B)の少なくともいっぽうはX/2X/2以上である。d(c,A)X/2d(c, A) \ge X/2としてよい。また、パスVbAV_b - Aが長さXX未満という仮定より

d(Vb,c)+d(c,A)<X d(V_b, c) + d(c, A) < X

が成り立つから、d(Vb,c)<X/2d(V_b, c) < X/2が導ける。整理すると

Dd(Vw,c)+d(Vb,c)<d(Vw,c)+X2d(Vw,c)+d(c,A)=d(Vw,A) \begin{aligned} D & \le d(V_w, c) + d(V_b, c) \\ & < d(V_w, c) + \frac{X}{2} \\ & \le d(V_w, c) + d(c, A) \\ & = d(V_w, A) \end{aligned}

d(Vw,A)>Dd(V_w, A) > Dが導かれたので、直径より長いパスが見つかったことになり、矛盾する。\square


  1. https://en.wikipedia.org/wiki/Tree_(graph_theory)#Properties より、For any three vertices in a tree, the three paths between them have exactly one vertex in common (this vertex is called the median of these three vertices). というやつ。 ↩︎

2020年11月21日土曜日

CODE FESTIVAL 2014 - 宝探し 2

変更なしかつ一次元で考えると種類数の有名問題になるが、お宝が高々100種類しかないので累積和の列を100個用意するだけでよい。また、隣接要素を入れ替える操作が累積和上でどうなっているか考えると、右から左に移動するお宝に関する累積和列の1個所が+1され、左から右に移動するお宝に関する累積和列の同じ個所が-1されるだけなので、特別なデータ構造なしで処理できる。

二次元の場合も、二次元累積和を取って同じことをやればよい。上下の入れ替えでは、下から上に移動するお宝に関する累積和配列の1個所からその行の右方にある数がすべて+1され、上から下に移動するお宝に関する累積和列の同じ個所からその行の右方にある数がすべて-1される。左右の入れ替えも同様に処理できる。計算量はO(nmk+qmax(n,m)){O}(nmk + q\max(n, m))

2020年11月20日金曜日

CS Academy Round #83 - Firestarter

着火クエリ(Ti,Ai,Bi,Xi)(T_i, A_i, B_i, X_i)に対して新しい頂点ViV_iを導入し、辺{Ai,Bi}\{A_i, B_i\}を分割して長さXiX_iの辺{Ai,Vi}\{A_i, V_i\}と長さLXiL-X_iの辺{Vi,Bi}\{V_i, B_i\}を張る。ひとつの辺上に複数の着火が起こる場合には、3つ以上に分割する。こうしてできたグラフ上で、各頂点に初めて火が到達する時間を求める。具体的には、ViV_iの距離をd[Vi]:=Tid[V_i] := T_iとセットして多点スタートのダイクストラをする。(形式的には、大頂点SSを導入して各ViV_iSSの間に長さTiT_iの辺があると考えるのがよさそう。)

こうすると、頂点vvに初めて火が到達する時間がd[v]d[v]に入ることになる。ただ、欲しいのはすべての辺が燃えきる時間なので、maxd[v]\max d[v]は答えにならない。長さllの辺{u,v}\{u, v\}が燃えきる時刻をttとすると(td[u])+(td[v])=l(t-d[u])+(t-d[v]) = lなので、t=(l+d[u]+d[v])/2t = (l + d[u]+d[v])/2と求まる。この最大値を答えとすればよい。


時刻を決め打って二分探索をやってしまって、さすがにTLEだった。

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]を処理した瞬間にしか成り立っていないことに注意。