- bitworld 的博客
【数论】阶乘取模:从入门到赛场实战
- 2025-4-26 10:03:28 @
阶乘取模:从入门到赛场实战
我们今天要解决的核心问题是计算 的值。其中 和 可能都很大。
为什么这个问题重要?因为组合计数问题(比如计算 )的核心就是阶乘及其逆元的计算。搞不定阶乘取模,组合计数基本就没法做。
零、 前置准备:快速幂与模逆元
在深入阶乘之前,确保你已经熟练掌握了两个基本工具:
- 快速幂 (Exponentiation by Squaring): 高效计算 。
- 模逆元 (Modular Inverse): 找到一个整数 使得 。
如果 是素数,根据费马小定理, 就是 模 的逆元(前提是 不被 整除)。如果 不一定是素数,但 ,则需要用扩展欧几里得算法 (Extended Euclidean Algorithm, exgcd) 求解 中的 。
一、 简单情况:p 为素数,且 n < p
这是最简单的情况。因为 且 是素数,所以 中没有任何一个是 的倍数。
直接计算即可:
$$n! \pmod p = (1 \times 2 \times \dots \times n) \pmod p $$实现非常直接:
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
// 快速幂 a^b mod p
ll power(ll a, ll b, ll p) {
ll res = 1;
a %= p;
while (b > 0) {
if (b & 1) res = (res * a) % p;
a = (a * a) % p;
b >>= 1;
}
return res;
}
// 求解 n! mod p, 当 n < p 且 p 是素数
ll fact_mod_simple(int n, ll p) {
// 输入:n, p (p为素数, n < p)
// 输出:n! mod p
if (n >= p) return 0; // 理论上不会进入,因为前提是 n < p
ll res = 1;
for (int i = 1; i <= n; ++i) {
res = (res * i) % p;
}
return res;
}
int main() {
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
int n;
ll p;
// 题意:计算 n! mod p,保证 p 是素数且 n < p
cin >> n >> p;
cout << fact_mod_simple(n, p) << endl;
return 0;
}
// 时间复杂度: O(n)
// 空间复杂度: O(1)
这个场景虽然简单,但它是后面更复杂情况的基础。
二、 理论基石:威尔逊定理 (Wilson's Theorem)
在讨论 的情况前,我们必须了解威尔逊定理。
威尔逊定理: 对于一个素数 ,有 。反之,若 且 ,则 是素数。
这个定理本身不直接计算 (除非 ),但它揭示了阶乘与素数模运算之间的深刻联系。它在某些特定问题的证明或推导中非常有用,例如后面我们会看到它在计算 时的一个关键应用。
三、 核心挑战:p 为素数,且 n >= p
当 时, 中必然包含因子 ,所以 。
这看起来好像问题解决了?但实际比赛中,几乎不会直接问你 的值,因为答案通常是 0。更常见的问题是:
- 计算组合数 。
- 计算 ,即从 中除去所有因子 后再取模。
这两个问题都需要我们更深入地理解 的结构。
3.1 勒让德定理 (Legendre's Formula)
首先,我们需要知道 中到底包含了多少个因子 。勒让德定理给出了答案。
勒让德定理: 素数 在 的素因子分解中出现的次数(即 的幂次),记作 ,等于:
$$v_p(n!) = \sum_{i=1}^{\infty} \lfloor \frac{n}{p^i} \rfloor = \lfloor \frac{n}{p} \rfloor + \lfloor \frac{n}{p^2} \rfloor + \lfloor \frac{n}{p^3} \rfloor + \dots $$这个和是有限的,因为当 时,。
理解: 计算了 中有多少个 的倍数。 计算了有多少个 的倍数。这些数至少贡献了两个因子 ,其中一个已经在 中计算过,这里补充计算了额外的那个因子 。 以此类推, 计算了 的倍数贡献的第 个因子 。
实现:
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
// 计算 v_p(n!)
ll legendre(ll n, ll p) {
// 输入: n, p (p为素数)
// 输出: n! 中因子 p 的个数
if (n < p) return 0;
ll cnt = 0;
while (n > 0) {
n /= p;
cnt += n;
}
return cnt;
}
int main() {
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
ll n, p;
// 题意: 计算 n! 中质因子 p 的数量
cin >> n >> p;
cout << legendre(n, p) << endl;
return 0;
}
// 时间复杂度: O(log_p n)
// 空间复杂度: O(1)
3.2 计算
这是处理 时最关键的计算之一。我们想计算 中所有不含因子 的部分的乘积模 。
令 。我们要求 。
考虑 。我们可以把这些数分成两类:
- 能被 整除的数: 。
- 不能被 整除的数。
$n! = (\text{不被 } p \text{ 整除的数的乘积}) \times (\text{被 } p \text{ 整除的数的乘积})$
$(\text{被 } p \text{ 整除的数的乘积}) = (p) \times (2p) \times \dots \times (\lfloor n/p \rfloor p)$ $= p^{\lfloor n/p \rfloor} \times (1 \times 2 \times \dots \times \lfloor n/p \rfloor)$ $= p^{\lfloor n/p \rfloor} \times (\lfloor n/p \rfloor)!$
代入 的表达式: $n! = (\text{不被 } p \text{ 整除的数的乘积}) \times p^{\lfloor n/p \rfloor} \times (\lfloor n/p \rfloor)!$
两边同时除以 : $f(n) = \frac{n!}{p^{v_p(n!)}} = (\text{不被 } p \text{ 整除的数的乘积}) \times \frac{p^{\lfloor n/p \rfloor} \times (\lfloor n/p \rfloor)!}{p^{v_p(n!)}}$
根据勒让德公式 $v_p(n!) = \lfloor n/p \rfloor + v_p(\lfloor n/p \rfloor!)$,我们有 $v_p(n!) - \lfloor n/p \rfloor = v_p(\lfloor n/p \rfloor!)$。 所以 $\frac{p^{\lfloor n/p \rfloor}}{p^{v_p(n!)}} = \frac{1}{p^{v_p(\lfloor n/p \rfloor!)}}$。
代入得到: $f(n) = (\text{不被 } p \text{ 整除的数的乘积}) \times \frac{(\lfloor n/p \rfloor)!}{p^{v_p(\lfloor n/p \rfloor!)}}$ $f(n) = (\text{不被 } p \text{ 整除的数的乘积}) \times f(\lfloor n/p \rfloor)$
现在我们需要计算 。 考虑 中的这些数。我们将它们按模 分组。 例如,考虑 。它们的乘积是 。 考虑 。模 后,它们对应 。所以它们的乘积模 也是 。 ... 考虑 $(\lfloor n/p \rfloor - 1)p + 1, \dots, \lfloor n/p \rfloor p - 1$。模 后也是 。乘积模 也是 。 最后,还有一部分数:。模 后对应 。它们的乘积是 。
所以, $= [(p-1)!]^{\lfloor n/p \rfloor} \times (n \pmod p)! \pmod p$
根据威尔逊定理,。 因此,$(\text{不被 } p \text{ 整除的数的乘积}) \pmod p \equiv (-1)^{\lfloor n/p \rfloor} \times (n \pmod p)! \pmod p$。
将这个结果代回 的递推式: $f(n) \pmod p \equiv [(-1)^{\lfloor n/p \rfloor} \times (n \pmod p)!] \times f(\lfloor n/p \rfloor) \pmod p$
这是一个递归计算 的方法。边界条件是 。
注意: 可以写成 如果 是奇数,写成 如果 是偶数。但有个坑:如果 ,那么 。所以在 的情况下,这一项总是 。不过更简单的处理是, 在模 意义下,只有当 且 为奇数时才等于 ,否则等于 。
实现:
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
// 预计算 1! mod p, 2! mod p, ..., (p-1)! mod p
vector<ll> fact;
ll P; // 模数 p
// 快速幂
ll power(ll a, ll b) {
ll res = 1;
a %= P;
while (b > 0) {
if (b & 1) res = (res * a) % P;
a = (a * a) % P;
b >>= 1;
}
return res;
}
// 递归计算 n! / p^(v_p(n!)) mod p
ll fact_mod_p_removed(ll n) {
// 输入: n
// 输出: n! 中除去所有因子 p 后,模 p 的值
if (n == 0) return 1;
// 计算 (n mod p)! mod p
ll term1 = fact[n % P];
// 计算 (-1)^(n/p) mod p, 注意 p=2 的情况
ll term2 = 1;
if ( (n / P) % 2 == 1 && P > 2) { // 只有 p > 2 且 n/p 是奇数时,才是 -1 (即 p-1)
term2 = P - 1;
}
// 另一种写法避免判断p=2:
// if ((n / P) & 1) term2 = P - 1; else term2 = 1;
// if (P == 2) term2 = 1; // 特判 p=2
// 递归计算 f(n/p) mod p
ll term3 = fact_mod_p_removed(n / P);
return (((term1 * term2) % P) * term3) % P;
}
int main() {
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
ll n;
// 题意: 计算 n! / p^(v_p(n!)) mod p,其中 p 是素数
cin >> n >> P;
// 预计算阶乘模 p
fact.resize(P);
fact[0] = 1;
for (int i = 1; i < P; ++i) {
fact[i] = (fact[i - 1] * i) % P;
}
cout << fact_mod_p_removed(n) << endl;
return 0;
}
// 时间复杂度: O(p + log_p n) (预处理 O(p), 递归 O(log_p n))
// 空间复杂度: O(p) (存储预处理的阶乘)
这个递归方法非常高效,时间复杂度是对数级的。
3.3 卢卡斯定理 (Lucas's Theorem)
卢卡斯定理是计算组合数 ( 为素数)的强大武器,尤其在 很大但 相对较小时。
卢卡斯定理: 对于非负整数 和素数 ,有:
$$\binom{n}{k} \equiv \binom{\lfloor n/p \rfloor}{\lfloor k/p \rfloor} \times \binom{n \pmod p}{k \pmod p} \pmod p $$理解: 这个定理将 的计算分解为两个更小的组合数计算:一个是规模缩小 倍的 $\binom{\lfloor n/p \rfloor}{\lfloor k/p \rfloor} \pmod p$,另一个是规模在 以内的 。
我们可以递归地应用这个定理,直到 都变成 0。
计算 其中 : 这需要计算 。 由于 ,所以 和 都不含因子 ,它们的模 逆元存在。我们可以预计算 ,然后用费马小定理(或 exgcd)求逆元。
实现:
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
ll P; // 模数 p
// 快速幂 a^b mod P
ll power(ll a, ll b) {
ll res = 1;
a %= P;
while (b > 0) {
if (b & 1) res = (res * a) % P;
a = (a * a) % P;
b >>= 1;
}
return res;
}
// 模逆元 a^-1 mod P (要求 P 是素数)
ll inv(ll a) {
return power(a, P - 2);
}
// 预计算阶乘和阶乘逆元模 P
vector<ll> fact, invFact;
// 计算 C(n, k) mod P, 其中 n, k < P
ll C_small(ll n, ll k) {
if (k < 0 || k > n) return 0;
if (k == 0 || k == n) return 1;
if (invFact[k] == 0 || invFact[n - k] == 0) { // 逆元不存在(理论上不会发生,因为 k, n-k < P)
return -1; // Error signal
}
// C(n, k) = n! / (k! * (n-k)!) = fact[n] * invFact[k] * invFact[n-k] mod P
return (((fact[n] * invFact[k]) % P) * invFact[n - k]) % P;
}
// 卢卡斯定理计算 C(n, k) mod P
ll lucas(ll n, ll k) {
// 输入: n, k, 全局 P (素数)
// 输出: C(n, k) mod P
if (k < 0 || k > n) return 0;
if (k == 0) return 1;
// 递归计算
ll C1 = C_small(n % P, k % P);
ll C2 = lucas(n / P, k / P);
return (C1 * C2) % P;
}
int main() {
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
ll n, k;
// 题意: 计算 C(n, k) mod p,其中 p 是素数
cin >> n >> k >> P;
// 预计算 0! 到 (P-1)! 模 P 及其逆元
fact.resize(P);
invFact.resize(P);
fact[0] = 1;
invFact[0] = 1; // 0! 的逆元认为是 1
for (int i = 1; i < P; ++i) {
fact[i] = (fact[i - 1] * i) % P;
// 注意: 必须在计算完所有 fact 后再计算 invFact,或者用 exgcd 单独求逆元
// 这里我们先算完 fact
}
// 计算 (P-1)! 的逆元,然后递推计算其他逆元
invFact[P - 1] = inv(fact[P - 1]);
for (int i = P - 2; i >= 1; --i) {
// inv((i)!) = inv((i+1)!) * (i+1) mod P
invFact[i] = (invFact[i + 1] * (i + 1)) % P;
}
cout << lucas(n, k) << endl;
return 0;
}
// 时间复杂度: 预处理 O(P log P) (费马小定理求逆元) 或 O(P) (递推求逆元), 查询 O(log_p n)
// 空间复杂度: O(P)
卢卡斯定理提供了一种在模素数 下计算大组合数的标准方法。
四、 进阶挑战:p 为合数
当模数 是合数时,情况变得复杂得多。费马小定理失效,威尔逊定理失效,卢卡斯定理也直接失效。
核心思想是:
- 将 进行素因子分解:。
- 分别计算 的值,得到 个结果 。
- 利用中国剩余定理 (Chinese Remainder Theorem, CRT) 将这 个同余方程: 合并成一个解 。这个 就是 。
现在,主要矛盾转化为如何计算 ,其中 是素数, 是正整数。
4.1 计算
这可以说是阶乘取模问题中最复杂的部分。我们不能简单地像模素数那样操作,因为模 意义下的逆元不一定存在(当数是 的倍数时)。
同样,我们将 分解: $n! = (\text{不含因子 } p \text{ 的项的乘积}) \times (\text{含有因子 } p \text{ 的项的乘积})$
令 。 $(\text{含有因子 } p \text{ 的项的乘积}) = (p) \times (2p) \times \dots \times (\lfloor n/p \rfloor p)$ $= p^{\lfloor n/p \rfloor} \times (\lfloor n/p \rfloor)!$
令 。 则 $n! \equiv g(n) \times p^{\lfloor n/p \rfloor} \times (\lfloor n/p \rfloor)! \pmod{P}$ (注意,这里的同余是在模 下讨论,但 可能非常大,这里只是形式化表示)
我们真正需要的是计算 的值。直接的递推似乎很困难。
一个更有效的方法是分别计算:
- : 中因子 的个数(用勒让德公式)。
- : 中除去所有因子 后,模 的值。
令 。 我们有递推关系: $n! = (\prod_{i=1, p \nmid i}^{n} i) \times (p \cdot 2p \cdot \dots \cdot \lfloor n/p \rfloor p)$ $n! = (\prod_{i=1, p \nmid i}^{n} i) \times p^{\lfloor n/p \rfloor} \times (\lfloor n/p \rfloor)!$
两边同除以 ,并利用 $v_p(n!) = \lfloor n/p \rfloor + v_p(\lfloor n/p \rfloor!)$: $\frac{n!}{p^{v_p(n!)}} = (\prod_{i=1, p \nmid i}^{n} i) \times \frac{p^{\lfloor n/p \rfloor}}{p^{v_p(n!)}} \times (\lfloor n/p \rfloor)!$ $F(n, p, k) \equiv (\prod_{i=1, p \nmid i}^{n} i) \times \frac{(\lfloor n/p \rfloor)!}{p^{v_p(\lfloor n/p \rfloor!)}} \pmod{p^k}$ $F(n, p, k) \equiv (\prod_{i=1, p \nmid i}^{n} i) \times F(\lfloor n/p \rfloor, p, k) \pmod{p^k}$
关键在于计算 。 我们将 分成若干个长度为 的块。 考虑一个完整的块 。其中与 互素的数的乘积模 是多少? $\prod_{i=mP+1, p \nmid i}^{(m+1)P} i \equiv \prod_{j=1, p \nmid j}^{P} (mP+j) \pmod P$ 这说明,在每个长度为 的完整区间内,与 互素的数的乘积模 是相同的!令这个值为 。
那么 可以写成: $= (\prod_{j=1, p \nmid j}^{P} j)^{\lfloor n/P \rfloor} \times (\prod_{i=\lfloor n/P \rfloor P + 1, p \nmid i}^{n} i) \pmod P$ $= Val^{\lfloor n/P \rfloor} \times (\prod_{j=1, p \nmid j}^{n \pmod P} j) \pmod P$
现在,我们需要计算两个部分:
- 。
- 对于 。
计算 可以通过暴力 完成。 计算 也可以暴力 完成。
结合起来,计算 的递归函数:
// 假设 P = p^k
ll calculate_F(ll n, ll p, ll P) {
if (n == 0) return 1;
// 计算 Term(n % P) = (product_{j=1, p not dividing j}^{n mod P} j) mod P
ll term_last_block = 1;
for (ll i = 1; i <= n % P; ++i) {
if (i % p != 0) {
term_last_block = (term_last_block * i) % P;
}
}
// 计算 Val = (product_{j=1, p not dividing j}^{P} j) mod P
// 这个 Val 可以在递归外预计算一次,或者每次递归都算?
// 实际上 Val 在模 p^k 下有个性质,可以优化,但我们先用暴力的思想
ll val_full_block = 1;
// 优化:由于 (m*p^k + j) mod p^k = j,所以完整的块的乘积在模 p^k 下都一样
// 且 Val = product_{j=1, p not dividing j}^{p^k} j mod p^k
// 这个值有个性质:若 p>2 或 k<3, Val = -1 mod p^k; 否则 Val = 1 mod p^k
// 我们这里先写成需要计算的样子
if (n >= P) { // 只有当 n >= P 时才需要计算完整块
for (ll i = 1; i <= P; ++i) {
if (i % p != 0) {
val_full_block = (val_full_block * i) % P;
}
}
} else {
val_full_block = 1; // 如果 n < P,没有完整的块
}
// 计算 Val^(n/P) mod P
ll full_blocks_prod = power(val_full_block, n / P, P); // 需要快速幂
// 递归计算 F(n/p, p, k)
ll recursive_part = calculate_F(n / p, p, P);
// 最终结果 F(n) = (Val^(n/P) * Term(n%P)) * F(n/p) mod P
ll res = (full_blocks_prod * term_last_block) % P;
res = (res * recursive_part) % P;
return res;
}
// 然后 n! mod P = (F(n, p, k) * p^(v_p(n!)) ) mod P ?
// 不对,直接这样乘 p^(v_p(n!)) 会出问题,因为 F(n, p, k) 是模 P 计算的
// 而且 p^(v_p(n!)) 可能 >= P
正确的处理 的方法:
-
计算 使用勒让德公式。记为
vp_n
. -
如果 ,那么 是 的因子, 。
-
如果 ,我们需要计算 。这才是难点。 我们需要计算 。 然后最终结果是 。这里的乘法可以直接做,因为 ,所以 不会引入模 的问题(即不会变成 0)。
如何计算 ? 上面推导的递推关系是正确的: $F(n, p, k) \equiv (\prod_{i=1, p \nmid i}^{n} i) \times F(\lfloor n/p \rfloor, p, k) \pmod{p^k}$
计算 : 令 . $= (\prod_{j=1, p \nmid j}^{P} j)^{\lfloor n/P \rfloor} \times (\prod_{j=1, p \nmid j}^{n \pmod P} j) \pmod P$
这个方法是可行的,但每次递归都需要 的计算(如果 和 都暴力算)。总复杂度会是 。如果 很大,这不行。
优化 的计算: 我们把 写成 ()。 考虑 中的数,按模 分类。
- ...
每一组完整的 的乘积模 是 。 但我们现在需要模 。
一个更标准的做法是(以计算 为例,这涉及 的计算):
扩展卢卡斯定理 (Extended Lucas Theorem) 的核心步骤
目标:计算 .
- 计算 。 记为 。
- 计算 中因子 的总幂次 。
- 计算 。
- 计算 。
- 计算 。
- 计算 和 。这需要 与 互素,这是由定义保证的。需要用 exgcd 求逆元。
- 最终结果是 $\binom{n}{m} \equiv N \times M^{-1} \times NM^{-1} \times p^E \pmod{p^k}$。
核心就是计算 。
计算 的高效方法
令 。 $F(n) \equiv (\prod_{i=1, p \nmid i}^{n} i) \times F(\lfloor n/p \rfloor) \pmod P$
$\prod_{i=1, p \nmid i}^{n} i = (\prod_{j=1, p \nmid j}^{P} j)^{\lfloor n/P \rfloor} \times (\prod_{j=1, p \nmid j}^{n \pmod P} j) \pmod P$
- :可以 预处理 for 。查询 O(1)。
- :可以 预处理一次得到 。
递归计算 的复杂度将是 。
实现扩展卢卡斯需要的函数:
power(a, b, mod)
: 快速幂exgcd(a, b, &x, &y)
: 扩展欧几里得算法inv(a, mod)
: 使用 exgcd 求模mod
的逆元legendre(n, p)
: 计算calc_F(n, p, pk)
: 计算C_mod_pk(n, m, p, pk)
: 计算CRT(remainders, moduli)
: 中国剩余定理合并结果ExtendedLucas(n, m, P)
: 主函数,分解 ,调用C_mod_pk
和CRT
。
calc_F(n, p, pk)
的实现:#include <bits/stdc++.h> using namespace std; using ll = long long; // 注意:这里的 pk = p^k (模数) // 快速幂 (模数是 pk) ll power(ll a, ll b, ll pk) { ll res = 1; a %= pk; while (b > 0) { if (b & 1) res = (res * a) % pk; a = (a * a) % pk; b >>= 1; } return res; } // 预计算 Term(m) = product_{j=1, p not dividing j}^{m} j mod pk // 预计算 Val = product_{j=1, p not dividing j}^{pk} j mod pk vector<ll> term_prod; ll val_full_block; ll P_prime; // 质数 p // 计算 n! / p^(v_p(n!)) mod pk ll calc_F(ll n, ll pk) { if (n == 0) return 1; // 计算 (product_{j=1, p not dividing j}^{n mod pk} j) mod pk ll last_block_prod = term_prod[n % pk]; // 计算 (Val)^(n/pk) mod pk ll full_blocks_prod = power(val_full_block, n / pk, pk); // 递归 F(n/p) ll recursive_part = calc_F(n / P_prime, pk); // 注意这里是 n / p // F(n) = (full_blocks_prod * last_block_prod) * F(n/p) mod pk ll res = (full_blocks_prod * last_block_prod) % pk; res = (res * recursive_part) % pk; return res; } // 预处理 Term 和 Val void precompute(ll p, ll pk) { P_prime = p; term_prod.resize(pk + 1); term_prod[0] = 1; for (ll i = 1; i <= pk; ++i) { if (i % p != 0) { term_prod[i] = (term_prod[i - 1] * i) % pk; } else { term_prod[i] = term_prod[i - 1]; } } val_full_block = term_prod[pk]; }
// --- 下面是组合数和 CRT 的部分,为了完整性也给出框架 ---
// Exgcd
ll exgcd(ll a, ll b, ll &x, ll &y) {
if (b == 0) {
x = 1; y = 0;
return a;
}
ll d = exgcd(b, a % b, y, x);
y -= (a / b) * x;
return d;
}
// Modular Inverse using Exgcd
ll mod_inv(ll a, ll mod) {
ll x, y;
ll d = exgcd(a, mod, x, y);
if (d == 1) {
return (x % mod + mod) % mod;
} else {
return -1; // Inverse does not exist
}
}
// Legendre's Formula
ll legendre(ll n, ll p) {
if (n < p) return 0;
ll cnt = 0;
while (n > 0) {
n /= p;
cnt += n;
}
return cnt;
}
// Calculate C(n, m) mod pk
ll C_mod_pk(ll n, ll m, ll p, ll pk) {
if (m < 0 || m > n) return 0;
if (m == 0 || m == n) return 1;
precompute(p, pk); // 预处理 Term 和 Val
ll vn = legendre(n, p);
ll vm = legendre(m, p);
ll vnm = legendre(n - m, p);
ll E = vn - vm - vnm; // p 的幂次
// 如果 p^E >= pk,结果可能为 0,但这里 E 不一定 >= k
// 我们需要计算 p^E mod pk
ll p_pow_E = power(p, E, pk); // 计算 p^E mod pk
ll fn = calc_F(n, pk);
ll fm = calc_F(m, pk);
ll fnm = calc_F(n - m, pk);
ll inv_fm = mod_inv(fm, pk);
ll inv_fnm = mod_inv(fnm, pk);
if (inv_fm == -1 || inv_fnm == -1) {
// 理论上不应发生,因为 F(x) 保证与 p 互质
return -1; // Error signal
}
ll res = fn;
res = (res * inv_fm) % pk;
res = (res * inv_fnm) % pk;
res = (res * p_pow_E) % pk; // 最后乘上 p^E
return res;
}
// 中国剩余定理 (CRT) - 求解 x = r_i (mod m_i)
// 返回解 x mod M,其中 M = product(m_i)
ll CRT(const vector<ll>& r, const vector<ll>& m) {
ll M = 1;
for (ll mod : m) {
M *= mod;
}
ll result = 0;
for (size_t i = 0; i < r.size(); ++i) {
ll Mi = M / m[i];
ll inv_Mi = mod_inv(Mi, m[i]); // Mi 在模 m[i] 下的逆元
if (inv_Mi == -1) return -1; // Error if inverse doesn't exist
result = (result + r[i] * Mi % M * inv_Mi % M) % M;
}
return (result + M) % M; // Ensure positive result
}
// 扩展卢卡斯主函数
ll extended_lucas(ll n, ll m, ll P_mod) {
if (m < 0 || m > n) return 0;
if (m == 0 || m == n) return 1 % P_mod; // 注意取模
if (P_mod == 1) return 0;
vector<pair<ll, ll>> factors; // 存储 P_mod 的质因子分解 {p, p^k}
ll temp_P = P_mod;
for (ll i = 2; i * i <= temp_P; ++i) {
if (temp_P % i == 0) {
ll pk = 1;
ll p = i;
while (temp_P % i == 0) {
pk *= i;
temp_P /= i;
}
factors.push_back({p, pk});
}
}
if (temp_P > 1) { // 剩余一个大的质因子
factors.push_back({temp_P, temp_P});
}
vector<ll> remainders;
vector<ll> moduli;
for (const auto& factor : factors) {
ll p = factor.first;
ll pk = factor.second;
ll rem = C_mod_pk(n, m, p, pk);
if (rem == -1) return -1; // Error in calculation
remainders.push_back(rem);
moduli.push_back(pk);
}
// 如果只有一个因子,CRT 不需要
if (remainders.size() == 1) {
return remainders[0];
}
return CRT(remainders, moduli);
}
int main() {
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
ll n, m, P_mod;
// 题意: 计算 C(n, m) mod P_mod, P_mod 可以是合数
cin >> n >> m >> P_mod;
cout << extended_lucas(n, m, P_mod) << endl;
return 0;
}
// 时间复杂度:
// 质因数分解 O(sqrt(P_mod))
// 对每个质因子 p^k:
// 预处理 O(p^k)
// C_mod_pk 计算 O(p^k + log_p n * log pk) (快速幂和递归调用)
// 求逆元 O(log pk) (exgcd)
// CRT O(num_factors * log P_mod)
// 主要瓶颈在于预处理的 O(p^k)。如果 P_mod 的某个质因子 p 对应的 p^k 很大,会超时。
// 空间复杂度: O(max(p^k)) (预处理 term_prod)
```
4.2 小结与注意事项 (合数模)
- 核心: 分解模数 ,计算模 下的组合数,用 CRT 合并。
- 难点: 计算 。
- 复杂度: 扩展卢卡斯的主要瓶颈在于计算模 的部分,特别是预处理阶乘相关项,复杂度与 相关。如果 过大(例如 或 ),则该算法可能超时。
- 逆元: 模 求逆元必须用 exgcd。
- 精度: 全程使用
long long
防止溢出。
五、 实战技巧与坑点
- 看清模数性质: 第一时间判断模数 是素数还是合数。决定了你使用 Lucas 还是 Extended Lucas。
- 预处理: 对于素数模 ,预处理 及其逆元是标准操作,复杂度 O(p)。对于扩展 Lucas,预处理 是关键,复杂度 O(p^k)。
- 边界情况: 等边界要正确处理。 。 if or .
- long long: 几乎所有中间计算都要用
long long
。 - 模运算时机: 每次乘法后都要及时取模,防止溢出。加法后也要
(a + b) % mod
,减法后要(a - b % mod + mod) % mod
防止负数。 - 复杂度意识: 估算 (或 ) 的大小,判断 O(p), O(log n), O(p^k) 等复杂度是否可以接受。
- 威尔逊定理的应用场景: 虽然不直接计算阶乘,但它在推导 时用到 。某些特殊题目可能直接考察威尔逊定理。
六、 总结
我们系统地梳理了阶乘取模的几种情况:
- , 是素数: 直接计算 。
- , 是素数:
- 。
- 计算 :使用基于威尔逊定理的递归方法 。
- 计算 :使用卢卡斯定理 。
- 是合数:
- 分解 。
- 计算 (扩展卢卡斯的核心步骤):
- 需要计算 。
- 复杂度依赖 的大小,预处理 。
- 使用 CRT 合并结果。