2019年10月8日火曜日
いろはちゃんコンテスト Day2 E - 連呼
AAAが登場しない列を数えて(NN+M−2)から引く方針が良さそう。
とりあえず、最初の文字と最後の文字の制限がないとして考えてみる。AAAが登場しない列とは、AとAAをM個のBの間に挿入してできる列である。Aをd個にわけるとして、Aがx個、AAがy個になったとすると、
x+2y=N
x+y=d
より、y=N−d,x=2d−Nと整理できる。AとAAの並べ方は(yd)通りあり、M個のBへの挿入の仕方は(両端を含めると)(dM+1)通りある。したがって、総和は∑d(dM+1)(yd)となる。(有効なdについてだけ計算する。)
さて、最初の文字がAで最後の文字がBという制限を満たすものを数える。最初がABから始まるパターンとAAから始まるパターンにわけて数えることにする。
最初がABから始まる場合、最後がBなので、AB·B·...·Bの·で示したM−1箇所から選んでAかAAを挿入することになる。総和は∑d(dM−1)(yd)である。ただし、x+2y=N−1,x+y=d だからy=N−1−dである。
最初がAAから始まる場合、AAB·B·...·Bの·で示したM−1箇所から選んでAかAAを挿入することになる。総和はやはり∑d(dM−1)(yd)である。ただし、x+2y=N−2,x+y=d だからy=N−2−dである。
2019年9月29日日曜日
ABC 142 E - Get Everything
Nが小さいので、dp[S]= 集合Sに含まれる宝箱を全部開けられるような鍵の選び方をした時の最小コスト、を目指したくなる。(不可能な場合は∞。)
O(2NM)のDP
鍵iで開けられる宝箱の集合をCiとするとき、dp[∅]:=0と初期化して
dp[S∪Ci]:=min(dp[S∪Ci],dp[S]+Ci)
をすべての鍵と集合について更新して求まるなら話は早い……が、これは集合の包含関係について全部処理しているわけではない。つまり、この方法で最終的に求まるのはdp[S]= 集合Sに含まれる宝箱を全部開けられて、Sに含まれない宝箱を一つも開けられないような鍵の選び方をした時の最小コスト、である。しかし、もう少し考えると、求めるのは全体集合に対する最小コストだったので、それでも問題ないことがわかる。1
コンテスト中はこの辺の理解にぱっと至らなくて、しばらく逡巡してしまった。
O(3N+M)のDP
最初に目指したdpを直接求める方法を考えてみる。dpをすべて∞に初期化して、
dp[∅]:=0
dp[Ci]:=ai
とする。(重複する場合は小さい方を選ぶ。つまり、正確にはdp[Ci]:=min(dp[Ci],ai)。)dpを上位集合に関してゼータ変換すると、dp[S]= 宝箱の集合Sを一つの鍵で開ける場合の最小コスト、になっている。あとは、
dp[S]:=T⊆Smin(dp[T]+dp[S∖T])
を使って順次最小値を確定していけばよい。部分集合の効率的な列挙のやり方は https://topcoder.g.hatena.ne.jp/jackpersel/20100804/1281196966 に書いてある。
何個か宝箱の集合クエリを受け取ってそれに対する最小コストを返す問題の場合は、最後にdpをゼータ変換すればよい。 ↩︎
2019年9月26日木曜日
ARC 100 E - Or Plus Max
以下、ビット列とそれが表す集合を同一視する。インデックスの集合S,T⊆{0,1,...,2N−1},∣S∣,∣T∣≤2が与えられたとき、S⊕Tを、インデックスi∈S∪Tの中からAiが大きいもの2つを残した集合とする。⊕は結合的で可換なのでゼータ変換が適用できる。つまり、
F(S):=⊕T⊆Sf(T)
という変換ができる。fはf(S):={S}でよい。
これでi∨j⊆Kを満たすi,jに対するAi+Ajの最大値が求まるが、実際にはi∨j≤Kを満たすi,jについて考えなくてはならない。i∨j≤K⇔k∈{0,1,...,K}が存在してi∨j=k⇔k∈{0,1,...,K}が存在してi∨j⊆kだから、i∨j≤Kを満たすi,jに関する最大値はmaxk≤K(F(k)の片方+F(k)のもう片方)として累積的に求まる。
高速ゼータ変換に関するメモ。高速ゼータ変換は各部分集合S⊆Uの各要素をちょうど一回ずつ重複なく処理するので、集合の合併を取る場合はそのままくっつけて良い。つまり、例えばg(S):=Sという関数を和集合についてゼータ変換したいとするとき、つまり
G(S):=T⊆S⋃g(T)
を求めるとき、和集合S1∪S2を得る操作で重複を除く必要はない。
Common Lispに関するメモ。扱う集合が小さいので珍しくunionを使う気になったら初歩的な失敗をした。非破壊的な合併を書いているつもりでなんとなく(sort (union list1 list2) ...)としたのだけど、unionはリストをコピーするとは限らないのでsortで元のリストが破壊される。(sort (nunion (copy-list list1) (copy-list2 list2)) ...)とするか、あるいはunionしてからcopy-listするのが正しかった。
非破壊的操作はコピーするとは限らないというポイント、わかっているつもりで普段は忘れている。(sort (mapcar ...) ...)みたいなのも(mapcar #'identity list)がlistをそのまま返すような最適化は許されているので、元のリストが保たれるとは限らない。(SBCLでは大丈夫だけど。)本質的には危険なコードをけっこう書いてしまっている気がする。
SBCLに関するメモ。SBCLのmergeはいちいちsb-kernel:specifier-typeを呼ぶので1回の呼び出しが重い。使う時には注意する必要がある。