(2n-1)!! (奇数階乗) を mod 2^64 で求める
いろいろ忘れないようにメモ✍しておくとよさそうなので書いておきます。
やること
に対して 以下の奇数をすべて乗算した奇数階乗 (奇数に対する二重階乗)
\begin{align}
(2n-1)!! = 1 \cdot 3 \cdot 5 \cdot \cdots (2n-3)(2n-1)
\end{align}
を で高速に求めます。具体的には として 時間です。
ここで の部分は多項式の乗算と平行移動 ( の係数から の係数を求める) にかかります。
やりかた
ソースコードです。Petrozavodsk Camp の問題で出たんですがジャッジがどこか分からない……。いちおう の範囲で愚直なのと比較して verify してあります。
uint64_t oddfact(uint64_t n) { if (n == 0) return 1; static uint64_t comb64[64][64]; if (comb64[0][0] == 0) { for (int i = 0; i < 64; ++i) { comb64[i][0] = comb64[i][i] = 1; for (int j = 1; j < i; ++j) { comb64[i][j] = comb64[i - 1][j] + comb64[i - 1][j - 1]; } } } using poly = array<uint64_t, 65>; poly p = {1}; uint64_t k = 0; for (int b = 63 - __builtin_clzll(n); b >= 0; --b) { if (n & (1ULL << b)) { for (int i = 63; i >= 0; --i) { p[i + 1] += 2 * p[i]; p[i] *= 2 * k + 1; } ++k; } if (b == 0) break; poly q = {}, r = {}; for (int i = 0; i < 64; ++i) { uint64_t pk = 1; for (int j = i; j >= 0; --j) { q[j] += p[i] * comb64[i][j] * pk; pk *= k; } } for (int i = 0; i < 64; ++i) { for (int j = 0; i + j < 64; ++j) { r[i + j] += p[i] * q[j]; } } p = r; k *= 2; } return p[0]; }
やってること
多項式 を以下のように定めます。要は から始まる 個の奇数の積です。
\begin{align}
P_k(x) = (2x+1)(2x+3)\cdots(2x+2k-1)
\end{align}
すると求めたい は になるので、これを計算すればよいです。ここで、すべての の出現に が掛かっていることから、 で考える場合 以上の高次の係数はすべて になります。したがって 次から 次までの係数を持っておけば を評価するには十分です。
この多項式 (の係数) は典型的な繰り返し二乗法の形で効率よく計算できます。
よって上のような多項式の計算を 回やれば目的の (の係数) が求められ、全体で 時間で が計算できました。