pi(v):=「i回目で数字がvとなる確率」とすると,漸化式pi+1(v)←∑w s.t. v∈D(w)pi(w)/∣D(w)∣ (D(v):=vの約数)が立つ.これに従うと求める値は∑w∈D(n)wpk(w)と書けるが,これを求めるにはnの約数を再帰的に因数分解する必要がありそうだし,それを除いてもO(∣D(n)∣k)は掛かりそう.約数の個数∣D(n)∣の評価は難しいようだが少なくとも間に合わない.ここで詰まった.
ここで「vの約数dを選ぶ」というのは,v=∏pikiと素因数分解したとき,d=∏piki′ s.t. ki′≤kiを選ぶ,すなわち各素因数piの個数ki′を決定することである.
例えばv=72=23×32のとき,約数は{1,2,3,4,6,8,9,12,18,24,36,72}であるが,これは23の約数{1,2,4,8}と32の約数{1,3,9}の積である.このように約数を選ぶことは各素因数の組からそれぞれ好きなものを選ぶことと等しい.
この決定は互いに独立であるから,「積の期待値=期待値の積」となり,E[v]=E[∏piki]=∏E[piki]という風に各素因数に対して求めた結果の積を取ればよい.
以下では素因数pについて求める.いま初期値nに含まれるpがl個のとき,dp[i][j]:=i回目で数字がpjとなる確率とおく.すると素朴にはdp[i+1][j]←∑j≤w≤ldp[i][w]/(j+1)と更新できる.この計算量はO(kl2)であり,lは最大でもO(log2n)なのでO(klog2n).
実際はこれだと間に合わない(手元やAtCoderだと1sだがCodeforces上だと3s.C++なら間に合うかも?)ため,DPを高速化する必要がある.これはjを降順に試すとき,dp[i+1][j]←dp[i+1][j+1]+dp[i][j]/(j+1)であることに着目するとwが不要になることを利用する.これにより計算量はO(klogn)となる.
これを各素因数について行うと計算量はO(Lklogn) (L :=素因数の個数)と書ける.素数を小さい順に14個掛け合わせると約1.3×1016>nであるからL<14であり間に合いそうな気がしてくる.以上から最初の素因数分解とあわせて全体の計算量はO(n+klogn).
初めての解答方式だったが,これは逆元を求めればよい.最初は「分数P/Qで保持して最後にP×inv(Q)しなければならない」と思っていたのだが,実際は分数p/qが出てくるたびにp×inv(q)として扱って大丈夫.逆元の実装はeditorialにもあるFermatの小定理版だとかなり低速だったので,拡張ユークリッド法によるものを用いた(初実装.参考記事).因みに他の人の提出を見ていると,modつきnCrを計算するときのように配列に前計算しているケースも多かった.
ACコード(TLEギリギリ): 解答例
追記(19/03/07):素因数分解でリストを作っている箇所を大きさを決め打った配列に直したら560ms速くなった.あるいは大きさを推定せずともHashtbl
を用いてもよさそう(手元で5回測定した平均だとテストケース"10^15 10^4"
で前者のほうが10ms早い程度).今後はこのような処理ではHashtbl
を用いたほうがよいのかもしれない?
約数を素因数毎に分解して考えるというのは盲点だった.一方で約数の個数を求める式も似た考えで導出できることから,そんなに突飛な考え方でもなかったのかもしれない.