前言:
组合数,大家初中就学过,从n个不同元素里选出m个元素的所有组合的个数,叫做n个不同元素中取出m个元素的组合数。用符号C(n,m) 表示。在数论里,组合数算是非常基础的,也经常使用的一种算法,这篇博客主要是讨论在算法题目中,经常使用的处理组合数的方式。(如有错误,欢迎大家留言指正)
方式一:递推公式
我们初中就学过,组合数有许多性质,也肯定知道组合数有一个递推公式。
简单解释一下,我们从 n 个物体里选出 m 个物体,那么对于 n 个物体中的其中一个物体,无疑就只有两种情况,选择了这个物体 or 没有选择这个物体,那么选择了这个物体就有 C(n-1,m-1)种方式(已经确定一个,再选 m-1 个),没有选择这个物体就有 C(n-1,m)种方式(没有确定的,要选 m 个),因此我们可以得到 C(n,m)=C(n-1,m-1)+C(n-1,m)。
在一般题目中,我们可以通过这个递推公式来预处理组合数,这样在之后使用组合数时就可以实现O(1)调用了。
代码段
for(int i=0;i<=n;i++)//先处理m=0的情况
C[i][0]=1;
for(int i=1;i<=n;i++)
for(int j=1;j<=i;j++)
C[i][j]=(C[i-1][j]+C[i-1][j-1])%mod;//递推
注意事项
因为递推公式要开二维,如果 n 很大,数组会爆,建议如果 n>=1e4 的话要谨慎使用,实测 int 数组 n>=3e4 会爆,long long 数组 n>=2e4 会爆,即使数组没爆,也有可能爆内存,所以 n>=1e4 要谨慎使用。
方式二:利用公式预处理
前面利用递推公式的预处理,因为是二维的,很容易爆内存,所以,我们要想其他办法,来预处理。我们初中就学过。
一般来说,我们只需要预处理出 1~n 的阶乘(通过递推),就可以通过上面的式子计算出 C(n,m) 了,但是在很多情况下,求组合数,往往伴随着取模操作,所以不能简单的通过上面的式子计算了。
因此这里就涉及了分数取模的问题,这样的话,在模数为素数时,我们可以根据费马小定理来求。
这里简要说明一下费马小定理(不涉及证明)。
费马小定理
若存在整数 a , p 且gcd(a,p)=1,即二者互为质数,则有a^(p-1)≡ 1(mod p)。
我们可以根据费马小定理推出,a(p-2) ≡ a-1 (mod p)。因此我们可以将(mod为素数) n!-1%mod 转化成 n!mod-2%mod 通过费马小定理,之后再用快速幂,就可以转化成整数取模了。
代码段
快速幂代码
long long quickpow(long long x,long long k)
{
long long res=1;
while(k)
{
if(k&1)//k为奇数
res=res*x%mod;
k>>=1;//k/2
x=x*x%mod;
}
return res;
}
预处理为下方代码
//A[]为阶乘,inv[]为阶乘倒数
A[0]=1;//0!=1
for(int i=1;i<=n;i++)//n!=(n-1)!*n
A[i]=A[i-1]*i%mod;
//费马小定理
inv[n]=quickpow(A[n],mod-2);//quickpow快速幂
for(int i=n-1;i>=0;i--)//1/n!*n=1/(n-1)!
inv[i]=inv[i+1]*(i+1)%mod;
组合数就可以由下方代码得到
long long getC(int n,int m)
{
//A[]为阶乘,inv[]为阶乘倒数
if(n==m||!m)
return 1;
else
return A[n]*inv[m]%mod*inv[n-m]%mod;
}
注意事项
费马小定理只能运用在两数互质的情况下,所以一般题目只有模数为素数时我们才能使用上面的方法。当然,也要注意 n 的大小,1e8以上最好别使用,会爆内存。
方式三:Lucas定理
当 n>=1e15 并且最好 p<=1e5时,可以用Lucas定理来求解这些大组合数。
Lucas定理如下(冯志刚《初等数论》)
我们可以将上面的定理化为 C(n,m)%p=C(n/p,m/p)*C(n%p,m%p)%p,这样我们就可以递归的求解了。
代码段
递归
long long C(long long n,long long m)//C(n,m)
{
if(n<m)
return 0;
long long x=1,y=1;
for(long long i=n;i>n-m;i--)//暴力求阶乘
x=x*i%mod;
for(long long i=m;i>0;i--)
y=y*i%mod;
return x*quickpow(y,mod-2)%mod;//quickpow快速幂
}
long long Lucas(long long n,long long m)//C(n,m)%mod
{
if(m==0)
return 1;
return C(n%mod,m%mod)*Lucas(n/mod,m/mod)%mod;
}
非递归
long long Lucas(long long n,long long m)//C(n,m)%p
{
//quickpow快速幂,A[]为阶乘
long long res=1;
while(n&&m)
{
long long nn=n%mod,mm=m%mod;
if(nn<mm)
return 0;
//1/n!%mod=n^(mod-2)%mod;
res=res*A[nn]*quickpow(A[mm]*A[nn-mm]%mod,mod-2)%mod;
n/=mod,k/=mod;
}
return res;
}
注意事项
Lucas定理只有当 n,m非常大,p比较小的时候才嫩使用,并且如果 p 不为素数还得通过质因数分解+中国剩余定理合并等方式求解(这里不展开了)。
总结
组合数处理方式比较多,每个方式都有自己适应的条件,我们使用的时候要具体问题具体分析,根据条件选择合适的方式。欢迎大家评论交流。