组合数取模
组合数取模的困难不在组合数本身,而在阶乘中的因子能否在当前模数下求逆。
1 从阶乘公式到模意义下的组合数
组合数定义为
(rn)=r!(n−r)!n!,
其中 0≤r≤n。
在普通整数运算中,分母一定能够整除分子;但在模运算中,除法必须转化为乘法逆元。如果分母与模数不互质,逆元就不存在,阶乘公式不能直接使用。
本章主线:阶乘与逆元 → 模素数下的组合数 → Lucas 定理 → 模质数幂的组合数 → CRT 合并 → ExLucas。
组合数取模的核心是根据模数的结构选择方法:
- 模数为质数时,可以使用逆元;
- n 很大而模数为较小质数时,使用 Lucas;
- 模数为质数幂时,先从阶乘中剥离该质因子;
- 模数为任意合数时,分解为若干质数幂,再用 CRT 合并。
2 模质数下的组合数
2.1 阶乘与阶乘逆元
设模数 p 为质数,并且 n<p。
由于 1,2,…,n 都不被 p 整除,n!、r! 与 (n−r)! 在模 p 下均可逆,因此
(rn)≡n!(r!)−1((n−r)!)−1(modp).
预处理阶乘 fac 与阶乘逆元 ifac 后,可以在 O(1) 时间内计算一次组合数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| ll qpow( ll a, ll k, ll mod ) { }
vector<ll> fac, ifac; ll P;
void init( int N, ll p ) { P = p; fac.assign( N + 1, 1 ); ifac.assign( N + 1, 1 );
for( int i = 1; i <= N; ++ i ) { fac[i] = fac[i - 1] * i % P; }
ifac[N] = qpow( fac[N], P - 2, P ); for( int i = N - 1; i >= 0; -- i ) { ifac[i] = ifac[i + 1] * ( i + 1 ) % P; } }
ll nCr( ll n, ll r ) { if( n < r || r < 0 ) return 0; return fac[n] * ifac[r] % P * ifac[n - r] % P; }
|
这套写法要求预处理范围内的阶乘不含模数 p,通常使用在 N<p 的情形。
2.2 为什么 n≥p 时会失效
当 n≥p 时,n! 含有质因子 p,于是
n!≡0(modp).
此时阶乘在模 p 下没有逆元,不能直接代入阶乘公式。
但这并不意味着组合数本身一定模 p 为 0。例如
(1p)=p≡0(modp),
而
(pp)=1.
需要进一步分析 n,r 的 p 进制结构,这就是 Lucas 定理处理的问题。
3 Lucas 定理
3.1 定理
设 p 为质数,将 n,r 写成 p 进制:
n=n0+n1p+⋯+nkpk,
r=r0+r1p+⋯+rkpk,
其中 0≤ni,ri<p。
Lucas 定理给出
(rn)≡i=0∏k(rini)(modp).
若某一位满足 ri>ni,对应的小组合数为 0,因此原组合数模 p 也为 0。
3.2 递归形式
取最低位可以得到
(rn)≡(rmodpnmodp)(⌊r/p⌋⌊n/p⌋)(modp).
不断除以 p,就能逐位处理 n 与 r。
3.3 为什么能够逐位拆开
在模 p 意义下,由二项式定理有
(1+x)p≡1+xp(modp),
因为中间的组合数 (ip) 都是 p 的倍数。
将 n 按 p 进制展开:
(1+x)n=i∏(1+x)nipi≡i∏(1+xpi)ni(modp).
比较 xr 的系数,就得到 Lucas 定理中的逐位乘积。
3.4 实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| ll qpow( ll a, ll k, ll mod ) { }
vector<ll> fac, ifac; ll P;
void init( int N, ll p ) { P = p; fac.assign( N + 1, 1 ); ifac.assign( N + 1, 1 );
for( int i = 1; i <= N; ++ i ) { fac[i] = fac[i - 1] * i % P; }
ifac[N] = qpow( fac[N], P - 2, P ); for( int i = N - 1; i >= 0; -- i ) { ifac[i] = ifac[i + 1] * ( i + 1 ) % P; } }
ll nCr( ll n, ll r ) { if( n < r || r < 0 ) return 0; return fac[n] * ifac[r] % P * ifac[n - r] % P; }
ll lucas( ll n, ll r ) { if( r == 0 ) return 1;
return nCr( n % P, r % P ) * lucas( n / P, r / P ) % P; }
|
使用这一实现前,需要调用 init(P - 1, P)。
递归层数为 O(logpn),但预处理需要 O(p) 时间与空间,所以它适合模数为较小质数的情形。
4 阶乘中的质因子个数
4.1 Legendre 公式
设 p 为质数,vp(n!) 表示 n! 中质因子 p 的指数。
n! 中每个 p 的倍数至少贡献一个 p,每个 p2 的倍数再额外贡献一个 p,以此类推,因此
vp(n!)=⌊pn⌋+⌊p2n⌋+⋯.
1 2 3 4 5 6 7 8 9
| ll cntp( ll n, ll p ) { ll res = 0;
while( n ) { res += n / p; n /= p; } return res; }
|
所以组合数中 p 的指数为
e=vp(n!)−vp(r!)−vp((n−r)!).
若要求模 pk,并且 e≥k,则
(rn)≡0(modpk).
4.2 为什么需要剥离质因子
设模数为 pk。
阶乘中的 p 不可逆,但剥离所有 p 以后,剩余部分一定与 pk 互质,因此存在逆元。
可以把阶乘写成
n!=pvp(n!)Up(n),
其中 Up(n) 表示将 n! 中所有质因子 p 剥离后的部分。
于是
(rn)=peUp(n)Up(r)−1Up(n−r)−1.
接下来的问题变成:如何计算 Up(n)(modpk)。
5 模质数幂的组合数
5.1 去除 p 因子的阶乘
令模数为 pk=pk。
在每个长度为 pk 的区间中,不被 p 整除的数模 pk 形成相同的一组剩余,因此可以按完整周期计算乘积。
对于尾部不足一个周期的部分,再单独枚举。
而所有 p 的倍数在除去一个 p 后,继续形成
⌊pn⌋!
中的同类问题,所以还要递归处理 n/p。
5.2 单个质数幂下的组合数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| ll qpow( ll a, ll k, ll mod ) { } ll inv( ll a, ll mod ) { }
ll fac( ll n, ll p, ll pk ) { if( n == 0 ) return 1;
ll res = 1;
for( ll i = 1; i <= pk; ++ i ) { if( i % p != 0 ) { res = res * i % pk; } }
res = qpow( res, n / pk, pk );
for( ll i = 1; i <= n % pk; ++ i ) { if( i % p != 0 ) { res = res * i % pk; } }
return res * fac( n / p, p, pk ) % pk; }
ll cntp( ll n, ll p ) { ll res = 0;
while( n ) { res += n / p; n /= p; } return res; }
ll nCr_pk( ll n, ll r, ll p, ll pk ) { if( n < r || r < 0 ) return 0;
ll a = fac( n, p, pk ); ll b = fac( r, p, pk ); ll c = fac( n - r, p, pk );
ll e = cntp( n, p ) - cntp( r, p ) - cntp( n - r, p );
ll res = a * inv( b, pk ) % pk * inv( c, pk ) % pk;
return res * qpow( p, e, pk ) % pk; }
|
这里的 inv 使用扩展欧几里得算法,因为模数 pk 不一定是质数;剥离 p 后的 a,b,c 都与 pk 互质,所以逆元存在。
fac 每层需要枚举 1 到 pk,递归深度为 O(logpn)。因此这份模板适合质数幂 pk 不太大的情形。
6 ExLucas
6.1 从合数模数拆成质数幂
设任意正整数模数 M 的标准质因数分解为
M=i=1∏spiki.
这些质数幂两两互质。
分别计算
xi≡(rn)(modpiki),
就得到同余方程组
⎩⎨⎧x≡x1(modp1k1),x≡x2(modp2k2),⋮x≡xs(modpsks).
最后使用 CRT 合并,就能得到 (rn)modM。
6.2 算法结构
ExLucas 可以概括为三个步骤:
- 将模数 M 分解为若干个质数幂;
- 对每个 piki 调用
nCr_pk;
- 使用 CRT 合并所有余数。
它不是把 Lucas 的递归式简单推广到合数模数,而是组合了质因数分解、模质数幂组合数与 CRT。
6.3 实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
| ll qpow( ll a, ll k, ll mod ) { } ll inv( ll a, ll mod ) { }
ll fac( ll n, ll p, ll pk ) { if( n == 0 ) return 1;
ll res = 1;
for( ll i = 1; i <= pk; ++ i ) { if( i % p != 0 ) { res = res * i % pk; } }
res = qpow( res, n / pk, pk );
for( ll i = 1; i <= n % pk; ++ i ) { if( i % p != 0 ) { res = res * i % pk; } }
return res * fac( n / p, p, pk ) % pk; }
ll cntp( ll n, ll p ) { ll res = 0;
while( n ) { res += n / p; n /= p; } return res; }
ll nCr_pk( ll n, ll r, ll p, ll pk ) { if( n < r || r < 0 ) return 0;
ll a = fac( n, p, pk ); ll b = fac( r, p, pk ); ll c = fac( n - r, p, pk );
ll e = cntp( n, p ) - cntp( r, p ) - cntp( n - r, p );
ll res = a * inv( b, pk ) % pk * inv( c, pk ) % pk;
return res * qpow( p, e, pk ) % pk; }
ll a[maxn], m[maxn]; ll crt( int cnt ) { }
ll exlucas( ll n, ll r, ll mod ) { ll tmp = mod; int cnt = 0;
for( ll p = 2; p * p <= tmp; ++ p ) { if( tmp % p != 0 ) continue;
ll pk = 1; while( tmp % p == 0 ) { pk *= p; tmp /= p; }
++ cnt; m[cnt] = pk; a[cnt] = nCr_pk( n, r, p, pk ); }
if( tmp > 1 ) { ++ cnt; m[cnt] = tmp; a[cnt] = nCr_pk( n, r, tmp, tmp ); }
return crt( cnt ); }
|
这里的 crt 使用 02_同余方程与逆元 中的模板。
该实现使用试除法分解模数,适合模数能够在可接受时间内完成质因数分解的情形。
7 方法选择
| 条件 |
方法 |
| n 较小,模数为质数,且预处理范围小于模数 |
阶乘与阶乘逆元 |
| n,r 很大,模数为较小质数 |
Lucas |
| 模数为质数幂 pk |
nCr_pk |
| 模数为任意合数 |
ExLucas |
| 模数很大且难以试除分解 |
先使用大整数质因数分解 |
需要注意:
- Lucas 要求模数是质数;
- 当前 Lucas 模板需要预处理到 p−1,因此 p 不能过大;
- 费马逆元只能用于模质数;
- 模 pk 时必须先剥离阶乘中的所有 p;
- ExLucas 中各个质数幂两两互质,所以使用普通 CRT,而不是 ExCRT;
- 当前
fac 的复杂度与 pk 成正比,质数幂过大时不能直接使用;
- 组合数满足 (rn)=0 当 r<0 或 r>n。
8 与前后内容的联系
组合数取模综合使用了前面多章的内容:
- 算术基本定理负责把模数分解成质数幂;
- 逆元负责处理可逆的阶乘部分;
- 费马小定理支持模质数下的阶乘逆元;
- Legendre 公式统计阶乘中的质因子指数;
- CRT 负责合并不同质数幂下的答案;
- 大整数模数的分解可以交给 Miller–Rabin 与 Pollard–Rho。
相关内容: