前言:
咳咳咳咳 ,最近瘟疫盛行,围观的记得要戴口罩。
求解组合数的方法大家应该都见了很多了,这篇博客将围绕这个问题进行归纳和深入学习。
问题:
给定
求解组合数
。
那么,什么是组合数?
那么,我们先列举两种种简单的求解组合数的办法。
第一种,暴力求解:
学过C语言循环语句的,应该都会知道的求解组合数的办法,当然是在结果比较小,不会溢出的情况下。
直接上代码了。
ll C(ll n,ll k)
{
ll ans=1;
for(ll i=1;i<=k;i++)(ans*=(n-i+1))/=i;
return ans;
}
要点就是乘一个除一个,先乘再除,这样可以保证除尽。
因为每
个数里就会有一个因子
,每
个数会有一个因子
,所以可以保证除尽。
第二种,杨辉三角:
(盗图,版权意识薄弱,感谢百度图片对ACM事业的支持)
然后我们知道第
行第
个数就是
。(程序员数数都是从
开始的)
稍微提一下,可以用
来归纳证明。
代码:
ll C[1005][1005];
ll initC(ll n)
{
for(int i=0;i<=n;i++)
{
for(int j=0;j<=i;j++)
{
if(i==0||j==0)C[i][j]=1;
else C[i][j]=(C[i-1][j-1]+C[i-1][j])%mod;
}
}
}
这样就可以预处理出杨辉三角,然后o(1)查询了,而且支持取模。
再来看看两种不实用的办法。
第一种,Stirling公式:
根据斯特林公式,有
,当
越大,两边的比值越近似于
。
他可以用来近似地估计大小,但是对于求组合数而言,暂时没有太大实际意义,就留意一下吧。
当然也是有应用的其他题目的:任意门。
第二种,NTT求组合数
根据二项式定理,
展开后的第
项就是
,所以可以做一次NTT,在
范围内求解,但前提模数要有原根。
这个方法纯属作者胡诌,因为有卢卡斯定理的存在,所以没有任何实际意义,可以不用留意。
接下来就是今天的正文了!!
先讲一则笑话放放松。
一天,狗熊在森林里便便,看到一只小白兔,问:“小白兔,你掉毛吗?”
小白兔说:“不掉。”
然后狗熊就拿起小白兔擦屁股。
第二天,狗熊在森林里吃东西,看到一只小松鼠,问:“小松鼠,你掉毛吗?”
小松鼠说:“不掉。”
然后狗熊就拿起小松鼠擦嘴。
“可是我是小白兔啊。”
咳咳,围观记得戴口罩,正文来了。
卢卡斯定理:要求 为素数,最坏情况
考虑公式
。
所以我们只要预处理出
到
的阶乘,对
取模。再预处理出
到
的阶乘模p意义下的逆元,就可以利用
来
求组合数了。
但是注意一个问题,这个方法的前提是要有逆元。
当
为合数的时候,不管
范围是多少,都有可能出现逆元不存在的情况,十分麻烦,所以我们先解决素数的情况。
当
是素数且
时,就可能出现逆元不存在的情况。(至少
的情况可以求了)
那么考虑当
为素数的时候怎么求,这就是卢卡斯定理了。
卢卡斯定理:
,其中
为素数。
然后就可以解决
的情况了,简洁板书,就不证明了,读者可以自证或者查阅资料。
有了这个定理之后,我们只要预处理出
的阶乘,就可以求解
很大的情况。而对于素数
很大的时候,
比较小,我们只要预处理到
的阶乘就够了,这种情况甚至不需要卢卡斯定理。
模板:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll fpow(ll a,ll n,ll mod)
{
ll sum=1,base=a%mod;
while(n!=0)
{
if(n%2)sum=sum*base%mod;
base=base*base%mod;
n/=2;
}
return sum;
}
ll inv(ll a,ll mod)
{
return fpow(a,mod-2,mod);
}
ll jie[1000005],rjie[1000005];
void init_jie(ll n,ll mod)
{
jie[0]=1;
for(ll i=1;i<=n;i++)jie[i]=jie[i-1]*i%mod;
for(ll i=0;i<=n;i++)rjie[i]=inv(jie[i],mod);
}
ll Lucas(ll n,ll k,ll mod)//返回n取k对mod取模
{
if(n<k)return 0;
if(n>=mod)return Lucas(n/mod,k/mod,mod)*Lucas(n%mod,k%mod,mod)%mod;
else return jie[n]*rjie[n-k]%mod*rjie[k]%mod;
}
int main()
{
ll T;
scanf("%lld",&T);
while(T--)
{
ll n,k,p;
scanf("%lld%lld%lld",&n,&k,&p);
init_jie(p,p);
printf("%lld\n",Lucas(n+k,k,p));
}
return 0;
}
扩展卢卡斯定理:不限制 的范围,复杂度
光有卢卡斯定理,对于
不为素数的情况,仍然无法求解,于是,就有了扩展卢卡斯定理。
这个方法可以在
的时间下处理出组合数。
但是和卢卡斯定理又是截然不同的两种方法。
我们将模数
用唯一分解定理展开成
,用素数幂的乘积的形式表示。
然后我们只要分别求解
的值,就可以用中国剩余定理求解了。
所以我们现在的问题就变成了如何求解
的值。
还是考虑组合数公式:
。
但是分母可能存在因子
,这样就不能求逆元了。所以我们把阶乘中的因子
都提取出来。
就变成了
。其中
表示去掉因子
后模
的值,
表示从
个阶乘中提取出来的因子
的个数。
这样左边那个分母就可以求逆元了。
所以现在还有两个问题:
1. 如何求解
。
2. 如果计算
。
第二个问题比较简单,如何求解
阶乘里有多少个因子
。
到
里能整除
的有
个。
到
里能整除
的有
个。
到
里能整除
的有
个。
直到
为止,然后把所有答案加起来就好了,复杂度是
的。
代码也很短。
ll getNumOfP(ll n,ll p)//返回n的阶乘里有多少个因子p
{
if(n<p)return 0;
return getNumOfP(n/p,p)+n/p;
}
然后最难解决的是第一个问题。
我们考虑
的阶乘,模
。
我们把所有
的倍数提取出来。
再把左边的因子三各拿一个出来。
然后可以看到,
是我们不要的,因为要算去除
的因子。然后
是
的阶乘,其实就是
来的,我们可以递归求解。
然后是最后一个部分,如果不去掉3的倍数的话,
和
模
下是同余的,去掉3的倍数之后发现依然是这样,这就有了一个循环节。所以我们只要遍历一遍
,就可以求出来了,这个算法主要的复杂度就在这里,如果要优化的话也是突破点,如果还能优化的话 。
最后的模板:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll fmul(ll x,ll y,ll mod)
{
ll tmp=(x*y-(ll)((long double)x/mod*y+1.0e-8)*mod);
return tmp<0?tmp+mod:tmp;
}
ll fpow(ll a,ll n,ll mod)
{
ll sum=1,base=a%mod;
while(n!=0)
{
if(n%2)sum=sum*base%mod;
base=base*base%mod;
n/=2;
}
return sum;
}
ll ex_gcd(ll a,ll b,ll& x,ll& y)
{
if(b==0)
{
x=1;y=0;
return a;
}
ll ans=ex_gcd(b,a%b,x,y);
ll tmp=x;
x=y;
y=tmp-a/b*y;
return ans;
}
ll inv(ll a,ll mod)//存在逆元条件:gcd(a,mod)=1
{
ll x,y;
ll g=ex_gcd(a,mod,x,y);
if(g!=1)return -1;
return (x%mod+mod)%mod;
}
ll a[100005],m[100005];
ll crt(ll *a,ll *m,ll n)//长度为0到n-1
{
ll M=1;
for(int i=0;i<n;i++)M=M*m[i];
ll ans=0;
for(int i=0;i<n;i++)
{
ll MM=M/m[i];
ans=(ans+fmul(fmul(a[i],MM,M),inv(MM,m[i]),M))%M;
}
return ans;
}
ll getNumOfP(ll n,ll p)//返回n的阶乘里有多少个因子p
{
if(n<p)return 0;
return getNumOfP(n/p,p)+n/p;
}
ll getJieWithoutP(ll n,ll p,ll P)//返回去掉因子p的n的阶乘模P
{
if(n==0)return 1;
ll g=1,T=n/P,Yu=n%P;
for(ll i=1;i<=P-1;i++)if(i%p)g=g*i%P;
g=fpow(g,T,P);
for(ll i=1;i<=Yu;i++)if(i%p)g=g*i%P;
return (g*getJieWithoutP(n/p,p,P))%P;
}
ll CmodP(ll n,ll k,ll p,ll P)
{
ll partWithoutP=fmul(fmul(getJieWithoutP(n,p,P),inv(getJieWithoutP(n-k,p,P),P),P),inv(getJieWithoutP(k,p,P),P),P);
ll partWithP=fpow(p,getNumOfP(n,p)-getNumOfP(n-k,p)-getNumOfP(k,p),P);
return fmul(partWithoutP,partWithP,P);
}
ll exLucas(ll n,ll k,ll p)
{
ll cnt=0;
for(ll i=2;i*i<=p;i++)
{
if(p%i==0)
{
ll P=1;
while(p%i==0){p/=i;P*=i;}
m[cnt]=P;
a[cnt++]=CmodP(n,k,i,P);
}
}
if(p>1){
m[cnt]=p;
a[cnt++]=CmodP(n,k,p,p);
}
return crt(a,m,cnt);
}
int main()
{
ll n,m,p;
scanf("%lld%lld%lld",&n,&m,&p);
printf("%lld\n",exLucas(n,m,p));
return 0;
}