競プロ過去問演習#2 - Makoto and a Blackboard

published: ,
last modified: .

lang: ja

category: competitive_programming

tags: codeforces ocaml

Makoto and a Blackboard

pi(v)p_i (v) \coloneqqii回目で数字がvvとなる確率」とすると,漸化式pi+1(v)w s.t. vD(w)pi(w)/D(w) (D(v)vp_{i+1}(v) \leftarrow \sum_{w \text{ s.t. } v \in D(w)} p_{i} (w) / |D(w)| \ (D(v) \coloneqq vの約数))が立つ.これに従うと求める値はwD(n)wpk(w)\sum _{w \in D(n)} w p _ k (w)と書けるが,これを求めるにはnnの約数を再帰的に因数分解する必要がありそうだし,それを除いてもO(D(n)k)O(|D(n)|k)は掛かりそう.約数の個数D(n)|D(n)|の評価は難しいようだが少なくとも間に合わない.ここで詰まった.

ここで「vvの約数ddを選ぶ」というのは,v=pikiv= \prod p_i ^ {k _ i}と素因数分解したとき,d=piki s.t. kikid = \prod p_i ^ {k'_i} \text{ s.t. } k' _ i \le k _ iを選ぶ,すなわち各素因数pip _ iの個数kik' _ iを決定することである. 例えばv=72=23×32v=72=2^3 \times 3^2のとき,約数は{1,2,3,4,6,8,9,12,18,24,36,72}\{1,2,3,4,6,8,9,12,18,24,36,72\}であるが,これは232^3の約数{1,2,4,8}\{1,2,4,8\}323^2の約数{1,3,9}\{1,3,9\}の積である.このように約数を選ぶことは各素因数の組からそれぞれ好きなものを選ぶことと等しい. この決定は互いに独立であるから,「積の期待値=期待値の積」となり,E[v]=E[piki]=E[piki]E[v]=E[\prod p _ i ^ {k _ i}] = \prod E[p _ i ^ {k _ i}]という風に各素因数に対して求めた結果の積を取ればよい[1]

以下では素因数ppについて求める.いま初期値nnに含まれるppll個のとき,dp[i][j]:=idp[i][j] := i回目で数字がpjp^jとなる確率とおく.すると素朴にはdp[i+1][j]jwldp[i][w]/(j+1)dp[i+1][j] \gets \sum_{j \le w \le l} dp[i][w]/(j+1)と更新できる.この計算量はO(kl2)O(kl^2)であり,llは最大でもO(log2n)O(\log_2 n)なのでO(klog2n)O(k \log ^2 n)

実際はこれだと間に合わない(手元やAtCoderだと1sだがCodeforces上だと3s.C++なら間に合うかも?)ため,DPを高速化する必要がある.これはjjを降順に試すとき,dp[i+1][j]dp[i+1][j+1]+dp[i][j]/(j+1)dp[i+1][j] \gets dp[i+1][j+1]+dp[i][j]/(j+1)であることに着目するとwwが不要になることを利用する[2].これにより計算量はO(klogn)O(k \log n)となる.

これを各素因数について行うと計算量はO(Lklogn)O(L k \log n) (L(L \coloneqq素因数の個数))と書ける.素数を小さい順に14個掛け合わせると約1.3×1016>n1.3 \times 10^{16} \gt nであるからL<14L \lt 14であり間に合いそうな気がしてくる.以上から最初の素因数分解とあわせて全体の計算量はO(n+klogn)O(\sqrt n + k \log n)

初めての解答方式だったが,これは逆元を求めればよい.最初は「分数P/QP/Qで保持して最後にP×inv(Q)P \times inv(Q)しなければならない」と思っていたのだが,実際は分数p/qp/qが出てくるたびにp×inv(q)p \times inv(q)として扱って大丈夫.逆元の実装はeditorialにもあるFermatの小定理版だとかなり低速だったので,拡張ユークリッド法によるものを用いた(初実装.参考記事).因みに他の人の提出を見ていると,modつきnCr_nC_rを計算するときのように配列に前計算しているケースも多かった.

ACコード(TLEギリギリ): 解答例

追記(19/03/07):素因数分解でリストを作っている箇所を大きさを決め打った配列に直したら560ms速くなった.あるいは大きさを推定せずともHashtblを用いてもよさそう(手元で5回測定した平均だとテストケース"10^15 10^4"で前者のほうが10ms早い程度).今後はこのような処理ではHashtblを用いたほうがよいのかもしれない?

約数を素因数毎に分解して考えるというのは盲点だった.一方で約数の個数を求める式も似た考えで導出できることから,そんなに突飛な考え方でもなかったのかもしれない.


  1. 記法は適当. ↩︎

  2. 加えて速度にはそこまで影響しなそうだがjjを降順にすることでiiの番地も不要になるため,解答ではそのようにしている. ↩︎