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から今までに受け取った通知の数の合計として一次元配列にまとめていた。確かにそれでよかった。

2020年12月26日土曜日

CodeChef - Counting Strings

問題: 与えられた文字列ssについて、左から比較しても右から比較しても辞書順で大きい文字列ttを数える。

ttの構成を考えると、2つの位置i,j(ij)i, j (i \le j)を境界にして、以下のように左から順に5つの部分に分解できる。(2番目と4番目は1文字)

  • ssと完全一致
  • s[i]<t[i]s[i] < t[i]
  • 文字自由
  • s[j]<t[j]s[j] < t[j]
  • ssと完全一致

N:=sN := |s|とし、0-basedで考える。f(i):=f(i) := 文字s[i]s[i]より大きい文字の数(s[i]=as[i] = \operatorname{a}ならf(i)=25f(i)=25)とすると、i,ji, jが異なる場合の数え上げはi<jf(i)f(j)26ji1\sum_{i < j} f(i)f(j)26^{j-i-1}である。あとはi,ji, jが一致する場合、つまりssttがちょうど1字異なる場合を別に数えてf(0)+...+f(N1)f(0)+...+f(N-1)を足す。

上の式を見ると、畳み込める形になっている。g(x)=f(Nx1)g(x) = f(N-x-1)として一方の添え字を逆順にすると、26N2i+jN2f(i)g(j)(1/26)i+j26^{N-2}\sum_{i+j \le N-2} f(i)g(j) (1/26)^{i+j}と変形できて、ffggをFFTで畳み込んだ後で1/261/26を掛けながら加算すればよい。mod\bmod109+710^9+7だが、登場する数が十分に小さい(高々25210525^2 \cdot 10^5)ので998244353998244353でやっても問題ない。


よく見ると、変形して累積和を取るだけだった。

i<jf(i)f(j)26ji1=126i=0N1(f(i)26ij=i+1N1f(j)26j) \sum_{i < j} f(i)f(j)26^{j-i-1} = \frac{1}{26}\sum_{i=0}^{N-1} \biggl( \frac {f(i)}{26^i} \sum_{j = i+1}^{N-1}{f(j)}26^j \biggr )

形を見て畳み込みできそうと思えるのは悪くないとしても、変数を分離できないかまず考えるほうがさすがによい気がする。

CodeChef - Two Closest

問題: 無向グラフG=(V,E)G=(V, E)VVの部分集合SS (special node)が与えられる。SS上の任意の2点間の距離で最短のものを求めよ。

方針: すべてのspecial nodeから各点への最短距離を調べるためにKK回ダイクストラする。この際、距離のテーブルは毎回使いまわしてよくて、枝刈りができる。また、special nodeを調べる順番をシャッフルすれば期待計算量が保証できる。

正当性について。今、special node A1,...,Ai1A_1, ..., A_{i-1}について二頂点間の最短距離の最小値が求まっているとし、距離のテーブルddd[v]:=vd[v] := vA1,...,Ai1A_1, ..., A_{i-1}との最短距離の最小値、という状態になっているとする。ここからさらにspecial node AiA_iを始点にダイクストラするとする。この際、頂点uuからvvを更新しようとした時にd[u]+w(u,v)d[v]d[u] + w(u, v) \ge d[v]であれば、vvは無視してよい。なぜならAiA_iから任意の頂点xxに到達する際の最短経路がvvを通るとしても、それ以下の距離でA1,...,Ai1A_1, ..., A_{i-1}のいずれかからxxに到達できることになるからだ。

計算量について。KK回ダイクストラをしている間に頂点vvがキューからポップされる回数の期待値を考える。vvがキューからポップされるのは、今調べているspecial nodeとvvの最短距離が以前調べたすべてのspecial nodeとvvの最短距離より小さい場合である。したがって、この期待値はKK個のspecial nodeとvvとの最短距離をd1,...,dKd_1, ..., d_Kとした時、(di)(d_i)をシャッフルした列のLDSの長さの期待値に等しく、O(K){O}(\sqrt K)らしい[1]。したがって、ポップに関する期待計算量はO(KNlogN){O}(\sqrt KN \log N)となる。また、ポップした頂点の隣接頂点を調べてdecrease keyする操作の期待計算量は、二分ヒープならO(KMlogN){O}(\sqrt KM \log N)となる。 したがって、全体の期待計算量はO(K(M+N)logN){O}(\sqrt K (M+N)\log N)ということになる。

waterfallsさんの別の乱択の方針も面白かった。special nodeをランダムに2分割して、一方を始点としたダイクストラをすると、最短距離を与える二頂点が異なるグループに属している時に正しい答えが出る。そうなる確率は1/2なので、何回もやって最小値を取ればよい。このテクニックは無向グラフの最短閉路アルゴリズムで見たことがある: http://research.haifa.ac.il/~raphy/papers/all-cycles.pdf


  1. この評価については https://www.math.ucdavis.edu/~romik/book/ のp.9に簡潔な証明が載っている。ランダム順列のLISの長さの期待値に関する研究はJ. M. Hammersley, A few seedlings of researchが初出らしい。(後者は読んでいない) ↩︎

2020年12月17日木曜日

JOI2019 本選 C - たのしいたのしいたのしい家庭菜園

左から列を作っていくとして、次にR, G, Yを隣に置くためにはまだ使っていないものの中で一番近いものを持ってくればよい。今までにR, G, Yをそれぞれ何個使ったか保持していれば一番近いR, G, Yがどこにあるかは決まるので、dp[x][y][z][c]:=\operatorname{dp}[x][y][z][c] :=Rをxx個、Gをyy個、Yをzz個使っていて直前に置いた色がccである場合の最小コスト、というDPがO(N3){O}(N^3)でできるはず……だが、それ以降のほうが難しかった。

例えばdp[x][y][z][c]\operatorname{dp}[x][y][z][c]から次にGを隣に持ってきてdp[x][y+1][z][G]\operatorname{dp}[x][y+1][z][G]に遷移するとき、何回スワップが必要か数える必要があるのだが、どうやって数えるかでしばらく悩んでいた。結論から言えば

  1. max(0,次のGの位置以前にあるRの総数x)\max(0, 次のGの位置以前にあるRの総数 - x)
  2. max(0,次のGの位置以前にあるYの総数z)\max(0, 次のGの位置以前にあるYの総数-z)

の和が間にあるG, Yの総数に等しく、これが必要なスワップの総数に等しい。

2020年12月12日土曜日

HTTF2021 決勝 - マイニング

入力の作り方を見る限り、鉱脈はある程度まとまっているので、鉱脈を連結成分として表現して、連結成分とその間のパスを効率よく取っていくとよさそう……だが言うは易しという感じの方針なので具体的にどう実装するかで悩み続けた。

  • 鉱脈を構成する点を判別していくつかの連結成分にわける
    • 定数α\alphaを決めて、pijC/α0p_{ij}-C/\alpha \ge 0である点(i,j)(i, j)を鉱脈を構成する点と考える。鉱脈を構成する二点で隣接しているものをUnionFindでつなぐ。コストC/2C/2で取れる点がある程度あることを考慮に入れようとしてα=1.5\alpha = 1.5くらいで始めたが、最終的には1.051.05になった。後から考えると、これは11で問題なかったと思う。
    • 連結成分はだいたい100個前後になる。
  • 2つの連結成分間の距離を求めておく
    • ある連結成分を始点とした場合の、グリッド上の各点への最短距離はダイクストラで求まるので、全連結成分間の距離をあらかじめ求めておく。連結成分は100個程度なので、100回程度ダイクストラすればよい。
    • 「グリッドの外周」もひとつの連結成分とみなして最短距離を求めておく。
    • ダイクストラをする際、鉱脈を構成する点は通れないことにしておく。こうしないと鉱脈を突っ切って別に鉱脈に行ったほうが得という場合にコストの推定を間違えてしまうし、そもそも負辺を含んでいることになるのでダイクストラできない。通れないことにしたせいで連結成分AAからCCへの移動が連結成分BBに阻まれても別に問題ない。(次のパートで、必要ならAAからBBBBからCCみたいな経路ができるはずなので。)
  • MST+ 焼きなまし
    • 使う連結成分が決まっているとき、推定スコアは、連結成分中のpijC/αp_{ij} - C/\alphaの総和から連結成分を頂点とする無向グラフ上のMSTの総コストを引いたものと考えられる。
    • 連結成分を使う/使わないで焼きなます。毎回クラスカルした。最初は単純に山登りしたが、焼きなましで多少スコアが上がった。
  • 基本の採掘経路を求める
    • 使う連結成分が決まったので、「グリッドの外周」を表す連結成分を根としてMST上をDFSしながら採掘していく。隣接する連結成分へ移動するための具体的な経路は、上と同じようにダイクストラして復元すれば得られる。
      • 頂点を取る順番もスコアに関係しそうだと思ったけれど、つめられず。
  • 貪欲で仕上げ
    • 既に採掘した点の隣接点でスコアが上がるものがあれば取る。
    • 既に採掘した点の隣接点とさらにその隣接点の2点を取ってスコアが上がるなら取る。
    • 以下、同様の貪欲を深さ4までやる。

267万点だった。


太字の部分に気づいて方針が決まるのに4時間、最初の提出までに6時間半かけてしまった。2~4時間のコンテストだと何もできずに終わってそう。

2020年12月5日土曜日

CS Academy Round #66 - Flipping Matrix

操作を逆順に見る。単位行列の行や列を入れ換えていくと、次のことがわかる:

  • 行と列の間に11による一対一対応があるという性質は交換で保たれる。(→ 置換行列
  • 任意の二行の交換はある二列の交換と同一操作であり、任意の二列の交換はある二行の交換と同一操作である。

逆に、初期配置から11を共通に含む行と列を1対1に対応させることができれば、この対応を1つの置換と考えることができ、互換を繰り返すことによって単位置換(=単位行列)に戻せる。また、あらゆる置換は高々N1N-1個の互換の積で表せるので、NN回以下という制限は問題にならない。結局、やることは次のようにまとまる:

  1. N×NN \times Nの二部グラフGGをつくり、Aij=1A_{ij}=1なら行に対応する頂点iiから列に対応する頂点jjに辺を張る。
  2. GGの最大二部マッチングMMを求める。MMが完全マッチングでなければ実行不能である。
  3. MMを一つの置換とみなして巡回置換に分解し、さらに互換に分解する。互換に従って行の交換を行う。

2020年12月2日水曜日

JOI2009 春合宿 - stamps

とりあえず最短作業時間は単純なDPで求まりそう。dp[x][y]:=S\operatorname{dp}[x][y] := Sを左からxx文字作り済みで、次に使える文字がOである(y=0y=0)あるいはIである(y=1y=1)場合の最小操作数、としてdp[N][0]\operatorname{dp}[N][0]が求める値である。使える文字と次に作りたい文字が一致していればコスト0で次に遷移できるし、一致していなければ文字を変更または挿入してコスト1で遷移できる。

また、元のIOI文字列をできるだけ短くするためには挿入の回数を最大化すればよくて、これも同様のDPができるが、最短作業時間のほうが優先なので上のDPと同時にやる必要がある。

形式的には、1つのDPと捉えて、DP配列に入っている値を(最短作業時間,挿入回数)(最短作業時間, 挿入回数)として辞書式の大小関係を与えると考えると、わかりやすいかもしれない。