1.组合数计算;
(1)通过定义式直接计算
根据定义,将n!、m!和(n-m)!计算出来即可;
long long C(long long n,long long m)
{
long long ans=1;
for(int i=1;i<=n;i++)
ans*=i;
for(int i=1;i<=m;i++)
ans/=i;
for(int i=1;i<=n-m;i++)
ans/=i;
return ans;
}
很明显,即使用long long型来保存,也只能承受n<=20的数据;
(2)通过递推公式计算(O(n^2))
由于表示从n个不同的数中选m个数的方案数,可以转换为两种选法方案数之和:一是不选最后一个数,从前n-1个数选m个数;二是选最后一个数,从前n-1个数选m-1,于是得到递推公式:
临界条件是,当m为0时或者m==n时,此时返回1
ll C(ll n,ll m)
{
if(m==0||n==m) return 1;
return C(n-1,m)+C(n-1,m-1);
}
这样不涉及阶乘计算,但是在递归过程中,我们可以发现会出现重复计算的情况。所以我们尝试用数组保存每次计算的组合数,如果有相同直接赋值即可;
ll res[70][70]={0,}
ll C(ll n,ll m)
{
if(m==0||n==m) return 1;
if(res[n][m]!=0) return res[n][m]
return res[n][m]=C(n-1,m)+C(n-1,m-1);
}
也可不使用递归,使用递推公式.
(3)通过定义的变形来计算(O(m))
由组合数的定义,可以进行化简,经过观察可以发现,分子分母都是m项,可以进行变形,即边乘边除的方法
由此写出代码:
ll C(ll n,ll m)
{
ll ans=1;
for(ll i=1;i<=m;i++)
ans=ans*(n-m+i)/i;
return ans;
}
虽然复杂度是O(m),但是程序有可能在最后一个乘法溢出,所以和(2)的范围基本相同。
列如方法2在n=67,m=33时开始溢出,而方法3在n=62,m=31时开始溢出。
2.计算%p
(1)通过递推公式计算
基于第一个问题的方法(2),是最容易、最实用的一种。只需在原先的地方对p取摸即可。
int res[1010][1010]={0,}
int C(int n,int m,int p)
{
if(m==0||n==m) return 1;
if(res[n][m]!=0) return res[n][m]
return res[n][m]=(C(n-1,m)+C(n-1,m-1))%p;
}
该算法支持m<=n<=1000,并且对于p的大小和素性没有额外限制(p<=10^9)。
(2)根据定义式计算(O(klog n)
将组合数进行质因子分解,然后利用快速幂计算每组,最后相乘再取一次模即可
质因子个数参考:
https://blog.csdn.net/qq_40725780/article/details/104210156
快速幂参加:
https://blog.csdn.net/qq_40725780/article/details/104237011
int prime[maxn] //素数表,注意表的最大素数不得小于n;
int C(int n,int m,int p)
{
int ans=1;
for(int i=0;prime[i]<=n;i++){
int c=cal(n,prime[i])-cal(m,prime[i])-cal(n-m,prime[i]); //获得prime[i]的指数
ans=ans*binaryPow(prime[i],c,p)%p;
}
return ans;
}
该算法支持m<=n<=10^6,并且对p的大小和素性没有额外限制
(3)通过定义式的变形计算
分情况讨论:
1.m<p,且是素数;(O(mlog m)
该做法基于第一个问题方法(3),但是不能在除法时直接模上p,因为每次的ans实际上已经取过模了。因此如果p是素数,可以使用扩展欧几里得算法或者费马小定理求出i模p的逆元;需要注意的时,此时必须满足m<p,否则中间求逆元过程可能失败
(p为素数,m<p, 两个条件都是确保逆元求解,保证gcd(m,p)=1)
逆元求解参考:
https://blog.csdn.net/qq_40725780/article/details/104225432
int C(int n,int m,int p)
{
int ans=1;
for(int i=1;i<=m;i++)
{
ans=ans*(n-m+1)%p;
ans=ans*inverse(i,p)%p;
}
return ans;
}
该算法支持m<=10^5的情况,且p必须为素数。
2.m任意(m>=p),p是素数(O(mlog n)
(这里情况下,如果按1的思想,不能保证在m的变换过程中,inverse(m,p)均可求)
因为m>=p,那么我们对组合数的的分子、分母求质因子的个数a,b。如果a>b,那么组合数的结果一定是p的倍数,直接返回0;如果a<=b,那么将分子中的p全部除去,保证分母的每个数都与p互质,即可正常计算
int C(int n,int m,int p)
{
int ans=1,nump=0;
for(int i=1;i<=m;i++){
int temp=n-m+1 //分子;
while(temp%p==0){
nump++;
tmep/=p;
}
ans=ans*temp%p; //获得分子中不含p的部分相乘;
temp=i; //分母;
while(temp%p==0){
nump--;
tmep/=p;
}
ans=ans*inverse(temp,p)%p; //除以分母中除了p以外的部分;
}
if(nump>0) return 0;
else retunr ans;
}
该算法支持m<=10^5,且p必须是素数
3.m任意,p任意
对分子,分母进行质因子分解获得化简后的结果,然后利用快速幂直接对p取模
(4)Lucas定理
p是素数(p<=10^5)
int Lucas(int n,int m)
{
if(m==0) return 1;
return C(n%p,m%p)*Lucas(n/p,m/p)%p;
}
这里的C可以方法二或者方法三情况1
总结情况