目录
一、组合数的简介
对于组合数相信大家都了解,这是数学排列组合经常要用到的。下面是组合数的计算公式!
对于组合数的公式来说,如果我们要计算它,关注到的有下面两个问题:
1.如何求出一个数的阶乘为多少?
2.当数字更大的话,如果最后的结果要对一个数进行取模应该怎么办?
下面就随着这两个问题展开我们今天的讨论吧!
二、问题求解的过程
①计算一个数的阶乘
因为直接利用阶乘计算思路非常简单,所以大家直接看代码动手操作就好了!
long long zhs(int n,int m) //计算组合数C nm。
{
//建立阶乘的数组,以及阶乘的逆元数组
long long fac[n],ifac[n];
fac[1]=1;
for(int i=2;i<=n;i++)
fac[i]=fac[i-1]*i;
long long ans;
ans=fac[n]/(fac[m]*fac[n-m]);
return ans;
}
int main()
{
cout<<zhs(5,3);
}
以上计算出来的结果便是组合数的结果,那么大家注意到还建立了另外一个ifac数组,至于这个数组的用处,就是接下来讨论的一个重点了。下面将介绍几个点依次求解最后的答案!
②欧拉定理
关注到组合数最后可能会很大,那么在最后我们需要对他进行取模处理,我们不可能在它计算出来之后才进行取模的处理,因为这样并没能达到防止数字溢出的可能,因此我们需要做的就是在程序运行计算的每一步进行一个取模的运算。而根据数学家们的研究,对于加减乘的运算,我们只需要直接在每一步取模即可,而对于除法的运算,则需要利用到欧拉定理的相关知识,求出一个数的逆元,然后让数字乘上逆元后进行取模,至于相关的欧拉定理的介绍,大家可以点一下链接。那么对于学算法的孩子来说,一般题目都会对一个质数进行取模,那么最后的逆元就是原数的(mod-1)次方,也就是就代表了x的逆元,而这里的mod一定是一个质数!
③快速幂
因为涉及到了求一个数字的多少次方,那么必不可少的是快速幂的相关介绍,之所以称其是快速幂当然就是因为它在计算上是比一般算法要快的,其时间复杂度只有O(logn)!下面先放上快速幂的实现,因为最后整体我们需要进行取模,所以其实在快速幂的运算上也是需要对每一步进行取模操作的。
//快速幂的实现 即求x的y次方
long long qbrt(int x,int y,int mod)
{
if(y==0) return 1; //特判
long long base=x;
long long ans=1;
while(y)
{
if(1&y) ans=(ans*base)%mod;
base=(base*base)%mod; //base不断地累乘自己,便于利用的时候乘上base。
y>>=1; //y不断的右移去找能够被利用的“1”的位置。
}
return ans;
}
快速幂的细节讲解
我们知道这里每一次对y进行右移一位,因此大大减小了时间复杂度。为什么可以这么做呢?因为每一个数y都可以写成二进制的形式,例如=××,注意到此处的数字了吗?124均是2的次方产生的数,而7的二进制本身就是111,也就是说把7拆成1+2+4之后,然后分别乘起来。而中间的base就是成2的指数次增长,也就满足了,当我搜到“1”的时候就可以乘上这个时候base对应的值了!
④阶乘逆元数组的求解
接下来还有一个需要解决的问题,就是我们如何把一个阶乘的逆元数组求出来呢?其实我们只需要把逆元先简单的理解成为一个数的倒数。也就是说,n!的逆元理解为1/n!,但是其实他计算出来的是n!的(mod-2)次方得到的数,但是如果我们这么理解之后,我们就会发现求解n以内的阶乘逆元便可以利用循环去求解,也就是下方的代码。
ifac[n]=qbrt(fac[n],mod-2,mod);
for(int i=n-1;i>=1;i--)
ifac[i]=ifac[i+1]*(i+1)%mod;
当然本蒟蒻认为如果你想利用循坏每次对一个数进行一次mod-2的快速幂应该也是可以的!
三、组合数取模的完整代码
#include<bits/stdc++.h>
using namespace std;
int n,m,mod;
//快速幂的实现 即求x的y次方
long long qbrt(int x,int y,int mod)
{
if(y==0) return 1; //特判
long long base=x;
long long ans=1;
while(y)
{
if(1&y) ans=(ans*base)%mod;
base=(base*base)%mod; //base不断地累乘x,便于利用的时候乘上base。
y>>=1; //y不断的右移去找能够被利用的“1”的位置。
}
return ans;
}
long long zhs(int n,int m,int mod) //计算组合数C nm。
{
//建立阶乘的数组,以及阶乘的逆元数组
long long fac[n],ifac[n];
fac[1]=1;
for(int i=2;i<=n;i++)
fac[i]=fac[i-1]*i;
ifac[n]=qbrt(fac[n],mod-2,mod);
for(int i=n-1;i>=1;i--)
ifac[i]=ifac[i+1]*(i+1)%mod;
long long ans;
ans=fac[n]*ifac[m]%mod*ifac[n-m]%mod;
return ans;
}
int main()
{
cin>>n>>m>>mod;
cout<<zhs(n,m,mod);
}
四、最终的结果
五、相关的例题
①水话
因为当时重新写组合数的代码应该是在交大新生程序设计热身赛的上面照猫画虎,最后还是艰难的AC了,因为那一题题目还是有点理解上的难度,而且在处理数据上也要多加注意,并且学到了一个计算过程中*1ll可以避免计算溢出int哦。
②原题呈现
因为原题太长的缘故,所以要看题的同学点这里!!!!!!!!!
③AC代码
#include<bits/stdc++.h>
using namespace std;
#define mod 998244353
int fac[1000005],ifac[1000005];
int qbow(int x,int y)//x的y次方
{
int ret=1,base=x;
while(y)
{
if(y&1) ret=1ll*ret*base%mod;
base=1ll*base*base%mod;
y=y>>1;
}
return ret;
}
void jc(int n)
{
fac[0]=1;
for(int i=1;i<=n;i++) fac[i]=1ll*fac[i-1]*i%mod;
ifac[n]=qbow(fac[n],mod-2);
for(int i=n-1;i>=0;i--)
ifac[i]=1ll*ifac[i+1]*(i+1)%mod;
}
long long zhs(int n,int m)
{
return 1ll*fac[n]*ifac[m]%mod*ifac[n-m]%mod;
}
int main()
{
int k;
int p,p1;
cin>>k;
int sum=0;
int s=0;
for(int i=1;i<=k;i++)
{
cin>>p1;
s+=p1;
}
cin>>p;
jc(s);
for(int i=1;i<=p;i++)
{
sum=1ll*(sum+zhs(s,i))%mod;
}
cout<<sum<<endl;
}
六、日常聊水
反正写到这里,终于又是把一个之前的知识点拿出来讲解了一遍,自己确实对这个知识又更加的牢固了,然后呢,应当在接下来的几天不会日常更新一篇模板类的博客啥的了,因为期末考试的科目需要复习等等,但是还是会坚持写一些题解吧,自己也发现必须每天进行题目的练习,要不然对于算法来说,自己的手就会感到非常的生疏!!