求组合数的几种方法

引入

在做题时,经常会遇到需要计算从 \(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;
}
posted @ 2023-03-16 16:21  ASnown  阅读(97)  评论(2编辑  收藏  举报