此处给出的参考实现在 O(p) 时间内预处理 p 以内的阶乘及其逆元后,可以在 O(1) 时间内计算单个组合数:
参考实现
#include <iostream>#include <vector>class BinomModPrime { int p; std::vector<int> fa, ifa; // Calculate binom(n, k) mod p for n, k < p. int calc(int n, int k) { if (n < k) return 0; long long res = fa[n]; res = (res * ifa[k]) % p; res = (res * ifa[n - k]) % p; return res; } public: BinomModPrime(int p) : p(p), fa(p), ifa(p) { // Factorials mod p till p. fa[0] = 1; for (int i = 1; i < p; ++i) { fa[i] = (long long)fa[i - 1] * i % p; } // Inverse of factorials mod p till p. ifa[p - 1] = p - 1; // Wilson's theorem. for (int i = p - 1; i; --i) { ifa[i - 1] = (long long)ifa[i] * i % p; } } // Calculate binom(n, k) mod p. int binomial(long long n, long long k) { long long res = 1; while (n || k) { res = (res * calc(n % p, k % p)) % p; n /= p; k /= p; } return res; }};int main() { int t, p; std::cin >> t >> p; BinomModPrime bm(p); for (; t; --t) { long long n, k; std::cin >> n >> k; std::cout << bm.binomial(n, k) << '\n'; } return 0;}
该实现的时间复杂度为 O(p+Tlogpn),其中,T 为询问次数。
exLucas 算法
Lucas 定理中对于模数 p 要求必须为素数,那么对于 p 不是素数的情况,就需要用到 exLucas 算法。虽然名字如此,该算法实际操作时并没有用到 Lucas 定理。它的关键步骤是 计算素数幂模下的阶乘。上文的第二个证明指出了它与 Lucas 定理的联系。
素数幂模的情形
首先考虑模数为素数幂 pα 的情形。将阶乘 n! 中的 p 的幂次和其他幂次分开,可以得到分解:
n!=pνp(n!)(n!)p.
其中,νp(n!) 为 n! 的素因数分解中 p 的幂次,而 (n!)p 显然与 p 互素。因此,组合数可以写作:
#include <iostream>#include <vector>// Extended Euclid.void ex_gcd(int a, int b, int& x, int& y) { if (!b) { x = 1; y = 0; } else { ex_gcd(b, a % b, y, x); y -= a / b * x; }}// Inverse of a mod m.int inverse(int a, int m) { int x, y; ex_gcd(a, m, x, y); return (x % m + m) % m;}// Coefficient in CRT.int crt_coeff(int m_i, int m) { long long mm = m / m_i; mm *= inverse(mm, m_i); return mm % m;}// Binominal Coefficient Calculator Modulo Prime Power.class BinomModPrimePower { int p, a, pa; std::vector<int> f; // Obtain multiplicity of p in n!. long long nu(long long n) { long long count = 0; do { n /= p; count += n; } while (n); return count; } // Calculate (n!)_p mod pa. long long fact_mod(long long n) { bool neg = p != 2 || pa <= 4; long long res = 1; while (n > 1) { if ((n / pa) & neg) res = pa - res; res = res * f[n % pa] % pa; n /= p; } return res; } public: BinomModPrimePower(int p, int a, int pa) : p(p), a(a), pa(pa), f(pa) { // Pretreatment. f[0] = 1; for (int i = 1; i < pa; ++i) { f[i] = i % p ? (long long)f[i - 1] * i % pa : f[i - 1]; } } // Calculate Binom(n, k) mod pa. int binomial(long long n, long long k) { long long v = nu(n) - nu(n - k) - nu(k); if (v >= a) return 0; auto res = fact_mod(n - k) * fact_mod(k) % pa; res = fact_mod(n) * inverse(res, pa) % pa; for (; v; --v) res *= p; return res % pa; }};// Binominal Coefficient Calculator.class BinomMod { int m; std::vector<BinomModPrimePower> bp; std::vector<long long> crt_m; public: BinomMod(int n) : m(n) { // Factorize. for (int p = 2; p * p <= n; ++p) { if (n % p == 0) { int a = 0, pa = 1; for (; n % p == 0; n /= p, ++a, pa *= p); bp.emplace_back(p, a, pa); crt_m.emplace_back(crt_coeff(pa, m)); } } if (n > 1) { bp.emplace_back(n, 1, n); crt_m.emplace_back(crt_coeff(n, m)); } } // Calculate Binom(n, k) mod m. int binomial(long long n, long long k) { long long res = 0; for (size_t i = 0; i != bp.size(); ++i) { res = (bp[i].binomial(n, k) * crt_m[i] + res) % m; } return res; }};int main() { int t, m; std::cin >> t >> m; BinomMod bm(m); for (; t; --t) { long long n, k; std::cin >> n >> k; std::cout << bm.binomial(n, k) << '\n'; } return 0;}
该算法在预处理时将模数 m 分解为素数幂,然后对所有 pα 预处理了自 1 至 pα 所有非 p 倍数的自然数的乘积,以及它在中国剩余定理合并答案时对应的系数。预处理的时间复杂度为 O(m+∑ipiαi)。每次询问时,复杂度为 O(logm+∑ilogpin),复杂度中的两项分别是计算逆元和计算幂次、阶乘余数的复杂度。