「ディバイド・アウト」問題解答 ( CodeIQ )
CodeIQというところでプログラミング問題「ディバイド・アウト」に解答しましたので、ネタバレです。
問題
今回は次のような問題でした。
- 空白区切りで入力される自然数 $n,p\,(1\le n\le 10^{12},\, 1\le p\le 10^5)$ に対し、$F(n,p)$の値を出力する
- $F(n,p)$とは $n!$を割り切れる限り$p$で割り、更にそれを$p$で割った時の剰余を表す。
例えば、$(n,p)=(12,5)$の場合、12!は5で2回割ることができ、$12!\div 5^2=19160064\equiv 4\,\,mod\,5$ であるため、$F(12,5)=4$と分かります。
解説
規則性の整理
まずは規則性を明らかにします。
上で挙げた例を少し変えて$F(13,5)$の場合ですが、13! を次のように整理してみます。 $$ \begin{eqnarray} &\,&13! \\ &=&1\cdot 2\cdot 3\cdot 4\cdot 5\cdot 6\cdot 7\cdot 8\cdot 9\cdot 10\cdot 11\cdot 12\cdot 13 \\ &=&(1\cdot 2\cdot 3\cdot 4)\cdot(6\cdot 7\cdot 8\cdot 9)\cdot(11\cdot 12\cdot 13)\cdot(5\cdot 10) \\ &=&(1\cdot 2\cdot 3\cdot 4)\cdot(6\cdot 7\cdot 8\cdot 9)\cdot(11\cdot 12\cdot 13)\cdot 5^2\cdot(1\cdot 2) \\ \end{eqnarray} $$
つまり、$p=5$の倍数かどうかで分けて見たことになります。
最終的には ( 割れるだけ割ってから更に ) $p=5$で割った余りを計算するため、1~4の積を6~9も同じと見做すようなことが、つまり $6\cdot 7\cdot 8\cdot 9\equiv 1\cdot 2\cdot 3\cdot 4$ や $11\cdot 12\cdot 13\equiv 1\cdot 2\cdot 3$ とすることができます。つまり、 $$ \begin{eqnarray} &\,&13!=(1\cdot 2\cdot 3\cdot 4)\cdot(6\cdot 7\cdot 8\cdot 9)\cdot(11\cdot 12\cdot 13)\cdot 5^2\cdot(1\cdot 2) \\ &\Leftrightarrow& \frac{13!}{5^2}\equiv (1\cdot 2\cdot 3\cdot 4)\cdot(6\cdot 7\cdot 8\cdot 9)\cdot(11\cdot 12\cdot 13) \cdot(1\cdot 2)\,\,mod\,5 \\ &\Leftrightarrow& \frac{13!}{5^2}\equiv (1\cdot 2\cdot 3\cdot 4)\cdot(1\cdot 2\cdot 3\cdot 4)\cdot(1\cdot 2\cdot 3) \cdot(1\cdot 2)\,\,mod\,5 \\ \end{eqnarray} $$
$F$での形は次の通りです。
- $F(13,5)\equiv F(4,5)^2 \cdot F(3,5) \cdot F(2,5)$
これを一般の$n,p$で書くと次のようになります。なお、$0!=1$ですから$F(0,p)=1$であり、$n$が$p$の倍数でも不整合はありません。
- $F(n,p)\equiv F(p-1,p)^{\lfloor n/p \rfloor} \cdot F(n\,mod\,p,p) \cdot F(\lfloor n/p \rfloor,p)\,\,mod\, p$
これを再帰的に繰り返していけば、それぞれ$p$未満の階乗の剰余の計算に落とすことができます。例えば、$F(1000,17)$であれば、$1000\div 17=58\cdots 14,~58\div 17=3\cdots 7$ と順に商・余りが出ますから、次のように計算できます。
$$ \begin{eqnarray} &\,&F(1000,17) \\ &=&F(16,17)^{58} \cdot F(14,17) \cdot F(58,17) \\ &=&F(16,17)^{58} \cdot F(14,17) \cdot ( F(16,17)^3 \cdot F(7,17) \cdot F(3,7) ) \\ \end{eqnarray} $$
ウィルソンの定理
ということで、この関係式が分かった時点で実装を進めるには十分なのですが。
- $F(0,p)=1$
- $F(n,p)\equiv F(p-1,p)^{\lfloor n/p \rfloor} \cdot F(n\,mod\,p,p) \cdot F(\lfloor n/p \rfloor,p)\,\,mod\, p$
べき乗の出てくる$F(p-1,p)\equiv(p-1)!\,\,mod\,p$ のところを更に簡略化する方法があります。それが次の式で表されるウィルソンの定理です。
- $(p-1)!\equiv -1\,\,mod\,p$
この証明については、ウィルソンの定理とその2通りの証明 | 高校数学の美しい物語などをご覧ください。
これを利用すると、次のようになります。
- $F(n,p)\equiv (-1)^{\lfloor n/p \rfloor} \cdot F(n\,mod\,p,p) \cdot F(\lfloor n/p \rfloor,p)\,\,mod\, p$
べき乗の部分は、指数の偶奇で符号が反転するだけなので、更に計算が楽になっています。
実装
さて、実装にあたっては上に上げた再帰を意識した訳なのですが、余り深く考えていなかった-1のべき乗を作るのに心理的な抵抗があったため、少しヒネリを加えました。
元の$F(n,p)$に類似した「$-1$~$-n$の積を$p$でできるだけ割り、それを$p$で割った余り」を$F_-(n,p)$とするのです。
要は、$F_-(n,p)\equiv (-1)^nF(n,p)\Leftrightarrow F(n,p)=(-1)^nF_-(n,p)$ ということなのですが、これも全く同じ議論により、
- $F_-(n,p)\equiv (-1)^{\lfloor n/p \rfloor} \cdot F_-(n\,mod\,p,p) \cdot F_-(\lfloor n/p \rfloor,p)\,\,mod\, p$
であることが分かります。
そうすると、-1のべき乗の部分は$F$あるいは$F_-$に吸収させて、
- $F(n,p)\equiv F(n\,mod\,p,p)\cdot F_-(\lfloor n/p \rfloor,p)\,\,mod\,p$
- $F_-(n,p)\equiv F_-(n\,mod\,p,p)\cdot F(\lfloor n/p \rfloor,p)\,\,mod\,p$
というように、$F,\,F_-$を交互に繰り返すような漸化式に置き換えることができます。この違いを第2引数y
で表現した再帰関数f
を実装したのが、以下の提出版Ruby(89)です。
終わりに
高速化についてはさっぱり吟味してなかったのですが、次のような情報が…
ディバイド・アウト、ideone で O(p^0.5 * (log(p) + log(N)/log(p))) ぐらいです: https://t.co/iPntHrKEOQ
— Min_25 (@min_25_) 2017年9月7日
上で整理した考えでは、せいぜい$O(p+log(n))$位までしかできなくて、それよりもずっと速いのですが、さっぱり何やってるのか分からないのが…。
ともあれ、色々な方のコードが CodeIQ「ディバイド・アウト」問題 みんなのコード - Togetterまとめ にまとめられていますので、一度ご覧になってはいかがでしょうか。