前置知识:阶乘取模

引入

本文讨论大组合数取模的求解。组合数,又称二项式系数,指表达式:

规模不大时,组合数可以通过 递推公式 求解,时间复杂度为 ;也可以在较大的素数模数 下,通过计算分子和分母的阶乘在 时间内求解。但当问题规模很大()时,这些方法不再适用。

基于 Lucas 定理及其推广,本文讨论一种可以在模数不太大 () 时求解组合数的方法。更准确地说,只要模数的唯一分解 中所有素数幂的和(即 )在 规模时就可以使用该方法,因为算法的预处理大致相当于这一规模。

Lucas 定理

首先讨论模数为素数 的情形。此时,有 Lucas 定理:

Lucas 定理

对于素数 ,有

其中,当 时,二项式系数 规定为

Lucas 定理指出,模数为素数 时,大组合数的计算可以转化为规模更小的组合数的计算。在右式中,第一个组合数可以继续递归,直到 为止;第二个组合数则可以直接计算,或者提前预处理出来。写成代码的形式就是:

示意

long long Lucas(long long n, long long k, long long p) {
  if (k == 0) return 1;
  return (C(n % p, k % p, p) * Lucas(n / p, k / p, p)) % p;
}

其中,C(n, k, p) 用于计算小规模的组合数。

递归至多进行 次,因而算法的复杂度为 ,其中, 为预处理组合数的复杂度, 为单次计算组合数的复杂度。

参考实现

此处给出的参考实现在 时间内预处理 以内的阶乘及其逆元后,可以在 时间内计算单个组合数:

该实现的时间复杂度为 ,其中, 为询问次数。

exLucas 算法

Lucas 定理中对于模数 要求必须为素数,那么对于 不是素数的情况,就需要用到 exLucas 算法。虽然名字如此,该算法实际操作时并没有用到 Lucas 定理。它的关键步骤是 计算素数幂模下的阶乘。上文的第二个证明指出了它与 Lucas 定理的联系。

素数幂模的情形

首先考虑模数为素数幂 的情形。将阶乘 中的 的幂次和其他幂次分开,可以得到分解:

其中, 的素因数分解中 的幂次,而 显然与 互素。因此,组合数可以写作:

式子中的 等可以通过 Legendre 公式 计算, 等则可以通过 递推关系 计算。因为后者与 互素,所以分母上的乘积的逆元可以通过 扩展欧几里得算法 计算。问题就得以解决。

注意,如果幂次 ,余数一定为零,不必再做更多计算。

一般模数的情形

对于 是一般的合数的情形,只需要首先对它做 素因数分解

然后,分别计算出模 下组合数 的余数,就得到 个同余方程:

最后,利用 中国剩余定理 求出模 的余数。

参考实现

最后,给出模板题目 二项式系数 的参考实现。

该算法在预处理时将模数 分解为素数幂,然后对所有 预处理了自 所有非 倍数的自然数的乘积,以及它在中国剩余定理合并答案时对应的系数。预处理的时间复杂度为 。每次询问时,复杂度为 ,复杂度中的两项分别是计算逆元和计算幂次、阶乘余数的复杂度。

习题