求组合数的几种方法
引入
在做题时,经常会遇到需要计算从 \(n\) 个物品中选择 \(m\) 个的方案数的情况。
我们就需要用到计算组合数的公式:\(\large{C_m^n} = \dfrac{n!}{(n-m)!m!}\)。这篇文章里为了方便,用 \(\dbinom{n}{m}\) 代替 \(\large{C_m^n}\)。
方法
1.乘法逆元
最简单的方法莫过于用组合数公式直接求。
但在 \(21!\) 就已经爆 long long,所以通常是会有模数的。
如果有了模数,那么普通的除法肯定是会出大问题,就又要引入逆元。
如果一个线性同余方程 \(ax \equiv 1 \pmod b\) ,则 \(x\) 称为 \(a \bmod b\) 的逆元,记作 \(a^{-1}\) 。
定义 \(fact_n = n!\bmod p\) (\(p\) 为模数),\(infact_n = (n!)^{-1}\bmod p\)。那么我们就可以把原始公式转换成 \(\dbinom{n}{m}=fact_n \cdot infact_{n-m} \cdot infact_m\)。
在这里,求乘法逆元可以用线性递推法 \(inv_i = (p-\lfloor\frac{p}{i}\rfloor)inv_{p\bmod i}\bmod p\)。
const int mod = ...;
int inv[],fact[],infact[];
void init(){
fact[1]=infact[1]=inv[1]=1;
for(int i=2;i<=...;i++){
inv[i]=(mod-mod/i)*inv[mod%i]%mod;
fact[i]=fact[i-1]*i%mod;
infact[i]=infact[i-1]*inv[i]%mod;
}
}
int C(int n,int m){
return (ll)fact[n]*infact[n-m]%mod*infact[m]%mod;
}
顺带一提
那么既然 \(20!\) 不爆 long long,在 \(n,m\le 20\) 的情况下你也可以写暴力。
2.预处理
在 \(n\) 和 \(m\) 都不是很大的情况下,如 \(n,m \le 10^4\),可以直接杨辉三角预处理
const int mod = ...;
int C[][];
void init(){
C[0][0]=1;
for(int i=1;i<=...;i++)
for(int j=0;j<=i;j++)
if(j)C[i][j]=(C[i-1][j-1]+C[i-1][j])%mod;
else C[i][j]=1;
}
int C(int n,int m){
return C[n][m];
}
3.Lucas定理
一般用于 \(n\),\(m\) 很大,\(mod\) 比较小的情况。
Lucas定理的证明和公式均在这篇博客中有,可自行跳转阅读,此处不再赘述。
具体求组合数的方法可以根据 \(mod\) 的范围自行选择,我这里选择了乘法逆元法。
const int mod = ...;
int inv[],fact[],infact[];
void init(){
fact[1]=infact[1]=inv[1]=1;
for(int i=2;i<=...;i++){
inv[i]=(mod-mod/i)*inv[mod%i]%mod;
fact[i]=fact[i-1]*i%mod;
infact[i]=infact[i-1]*inv[i]%mod;
}
}
int C(int n,int m){
return (ll)fact[n]*infact[n-m]%mod*infact[m]%mod;
}
int Lucas(int n,int m){
if(n<mod)return C(n,m);
return C(n%mod,m%mod) * Lucas(n/mod,m/mod) %mod;
}