2020年12月28日月曜日

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

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

J - 長い長い文字列

各操作で文字列の長さがどうなるかは先頭からシミュレーションすればわかるので、事前に求めておく。(大きな数になりえるので、適当に上限を決めてmin\minを取る。)

ii番目の操作で長さがLiL_iになるとする。列(Li)(L_i)XX以上である最小の位置iiを求めて、その位置にアルファベットがあればそれが答えである。数字の場合は、この操作を行う前の長さをllとするとき、XX文字目は(X1)%l(X-1) \% l文字目と一致しているはずである。したがって、X:=(X1)%lX := (X-1) \% lとして同じことを繰り返せばよい。ただし、00文字目はll文字目と考える。

L - T消し

TDPCのiwiを思い出して見に行ったらそのものだった。

M - 棒の出荷

最小値を決め打って判定問題を解く。dp[x]:=\operatorname{dp}[x] :=位置xxで区切れるならtrue、不可能ならfalseというDPを考えると、適当な区間[l(x),r(x))[l(x), r(x))についてdp[x]=dp[l(x)]dp[l(x)+1]...dp[r(x)1]\operatorname{dp}[x] = \operatorname{dp}[l(x)] \lor \operatorname{dp}[l(x)+1] \lor ... \lor \operatorname{dp}[r(x)-1]という遷移になる。コンテスト中はすぐにセグ木を貼ったが、累積和上でDPするとか、([l[x],r[x])[l[x], r[x])xxについて単調なので)スライドさせながらDPするとか、なんでもできそう。

O - 通知

愚直に処理すると、次数の大きい頂点が何回も更新されるときに困るが、次数の大きい頂点はそんなに多くなりえないことに注目する。閾値DDを決めてそれより次数の大きい頂点とそうでない頂点をわけて処理してみる。以下、次数がDDより大きい頂点をlarge node、小さい頂点をsmall nodeと呼ぶことにする。

通知クエリを処理するとき、small nodeからは周囲に愚直に通知を送り(small nodeなので送るべき頂点は多くない)、large nodeからは送らずに自身にためておいて、消化クエリが来た時に受け取る側から読みに行きたい(large nodeは多くないので毎回全部調べてもよい)。そこで、3つの配列を管理する:

  • unread[x]:=\operatorname{unread}[x] := small nodeからxxに送られた通知で未読のものの総数
  • send[x]:=\operatorname{send}[x] := large nodeであるxxが周囲に送った通知の総数
  • read[x][y]:=\operatorname{read}[x][y] := 頂点xx がlarge node yyから受け取った通知で既読のものの総数。

具体的な処理は次のようになる:

  • xxに対する通知クエリ
    • xxがsmall nodeなら、xxに接続しているすべての頂点yyについて、unread[y]+=1\operatorname{unread}[y] \mathrm{+}{=} 1する。
    • xxがlarge nodeなら、send[x]+=1\operatorname{send}[x] \mathrm{+}{=} 1する。
  • xxに対する消化クエリ
    • small nodeからの通知の総数を得る: unread[x]\operatorname{unread}[x]を読んだあと、00にリセットする
    • large nodeからの通知の総数を得る: xxに接続しているすべてのlarge node yyについて、send[y]\operatorname{send}[y]から既読の通知の総数read[x][y]\operatorname{read}[x][y]を引いたものの総和を取る。read[x][y]:=send[y]\operatorname{read}[x][y] := \operatorname{send}[y]として、既読を更新しておくこと。

計算量について考える。辺が1つ増えると全体の次数が2増えるので、次数の総和は2M2Mである。したがって、次数BB以上の頂点、つまりlarge nodeは、高々2M/B2M/B個しかない。クエリあたりの計算量は以下のように評価できる:

  • vvに対する通知クエリ
    • vvがsmall nodeならO(D)O(D)
    • vvがlarge nodeならO(1)O(1)
  • vvに対する消化クエリ
    • small nodeからの通知の合計を得るのはO(1){O}(1)
    • large nodeからの通知の合計を得るのはO(M/D){O}(M/D)

結局、全体の計算量はO(N+M+Q(D+M/D)){O}(N+M+Q(D+M/D))と書けて、D=MD= \sqrt MならO(N+M+QM{O}(N+M+Q\sqrt M)。

解説の実装はread[x][y]\operatorname{read}[x][y]を頂点xxがlarge nodeから今までに受け取った通知の数の合計として一次元配列にまとめていた。確かにそれでよかった。