组合数取模与Lucas定理
一句话说明
组合数题真正麻烦的通常不是
C(n,k)本身,而是“n很大,还要求对质数p取模”。
什么时候该想到它
- 题目要求
C(n,k) mod p n很大,不能直接算完整阶乘p是质数- 显式提示 Lucas 定理
普通组合数为什么不能直接照公式写
组合数公式是:
C(n, k) = n! / (k! * (n-k)!)麻烦有两个:
- 阶乘增长太快
- 模意义下不能直接做除法,要改成乘逆元
所以模质数时,真正写法是:
C(n, k) mod p
= n! * (k!)^(-1) * ((n-k)!)^(-1) mod p情况一:n 不大,直接预处理阶乘和逆阶乘
如果 n 规模可以接受,最稳的做法就是:
- 预处理
fact[i] = i! mod p - 预处理
invFact[i] = (i!)^(-1) mod p - 查询时
O(1)套公式
Go 模板:小范围组合数取模
func PrepareComb(limit, mod int) ([]int, []int) {
fact := make([]int, limit+1)
invFact := make([]int, limit+1)
fact[0] = 1
for i := 1; i <= limit; i++ {
fact[i] = fact[i-1] * i % mod
}
invFact[limit] = FastPow(fact[limit], mod-2, mod)
for i := limit; i >= 1; i-- {
invFact[i-1] = invFact[i] * i % mod
}
return fact, invFact
}
func CombModSmall(n, k, mod int, fact, invFact []int) int {
if k < 0 || k > n {
return 0
}
return fact[n] * invFact[k] % mod * invFact[n-k] % mod
}
func FastPow(a, b, mod int) int {
result := 1
a %= mod
for b > 0 {
if b&1 == 1 {
result = result * a % mod
}
a = a * a % mod
b >>= 1
}
return result
}情况二:n 很大,Lucas 定理登场
Lucas 定理适用于:
p 是质数它把一个大组合数拆成很多个“小于 p 的组合数”。
如果把 n 和 k 写成 p 进制:
n = ... n2 n1 n0
k = ... k2 k1 k0那么:
C(n, k) ≡ Π C(ni, ki) (mod p)一个直觉例子
以 p = 5 为例:
23 = 4 * 5 + 3
11 = 2 * 5 + 1所以:
C(23,11) mod 5
= C(4,2) * C(3,1) mod 5这就是 Lucas 的拆位思想。
Go 模板:Lucas
func Lucas(n, k, p int) int {
if k == 0 {
return 1
}
return CombPrime(n%p, k%p, p) * Lucas(n/p, k/p, p) % p
}
func CombPrime(n, k, p int) int {
if k < 0 || k > n {
return 0
}
fact := make([]int, p)
invFact := make([]int, p)
fact[0] = 1
for i := 1; i < p; i++ {
fact[i] = fact[i-1] * i % p
}
invFact[p-1] = FastPow(fact[p-1], p-2, p)
for i := p - 1; i >= 1; i-- {
invFact[i-1] = invFact[i] * i % p
}
return fact[n] * invFact[k] % p * invFact[n-k] % p
}为什么 Lucas 能工作
它本质上是在说:
- 大问题拆成每一位的小问题
- 每一位都只处理
0..p-1范围内的组合数
所以当 n 大得根本预处理不到时,Lucas 才有价值。
易错点
组合数取模与 Lucas 最容易错的地方
- Lucas 要求模数
p是质数。- 如果某一位上
ki > ni,那一位组合数就是0,整体直接为0。- 模意义下“除法”本质是乘逆元,不要直接写整数除法。
p很小时可以反复现算,小心常数;如果查询很多次,可以把0..p-1预处理复用。
复杂度
预处理方案
- 预处理:
O(n) - 单次查询:
O(1)
Lucas
- 拆位层数:
O(log_p n) - 每层计算一个小组合数
相关笔记
返回:数学算法