组合数求解与(扩展)卢卡斯定理

前言:

咳咳咳咳 ,最近瘟疫盛行,围观的记得要戴口罩。
求解组合数的方法大家应该都见了很多了,这篇博客将围绕这个问题进行归纳和深入学习。
 

问题:

给定 n , k , p n,k,p n,k,p求解组合数 C n k ( m o d    p ) C_{n}^{k}(mod\;p) Cnk(modp)
 
 
那么,什么是组合数?
那么,我们先列举两种种简单的求解组合数的办法。
 
 
第一种,暴力求解:
学过C语言循环语句的,应该都会知道的求解组合数的办法,当然是在结果比较小,不会溢出的情况下。
直接上代码了。

ll C(ll n,ll k)
{
    ll ans=1;
    for(ll i=1;i<=k;i++)(ans*=(n-i+1))/=i;
    return ans;
}

要点就是乘一个除一个,先乘再除,这样可以保证除尽。
因为每 2 2 2个数里就会有一个因子 2 2 2,每 3 3 3个数会有一个因子 3 3 3,所以可以保证除尽。
 
 
第二种,杨辉三角:
组合数求解与(扩展)卢卡斯定理_第1张图片
(盗图,版权意识薄弱,感谢百度图片对ACM事业的支持)
然后我们知道第 n n n行第 k k k个数就是 C n k C_{n}^{k} Cnk。(程序员数数都是从 0 0 0开始的)
稍微提一下,可以用    C n − 1 k − 1 + C n − 1 k = C n k    \;C_{n-1}^{k-1}+C_{n-1}^{k}=C_{n}^{k}\; Cn1k1+Cn1k=Cnk来归纳证明。

代码:

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公式:
根据斯特林公式,有    n ! ∼ 2 π n ( n e ) n \;n!\sim \sqrt{2\pi n}\left ( \frac{n}{e}\right )^{n} n!2πn (en)n,当 n n n越大,两边的比值越近似于 1 1 1
他可以用来近似地估计大小,但是对于求组合数而言,暂时没有太大实际意义,就留意一下吧。
当然也是有应用的其他题目的:任意门。

第二种,NTT求组合数
根据二项式定理, ( 1 + x ) n (1+x)^{n} (1+x)n展开后的第 k k k项就是 C n k C_{n}^{k} Cnk,所以可以做一次NTT,在 o ( n l o g n ) o(nlogn) o(nlogn)范围内求解,但前提模数要有原根。
这个方法纯属作者胡诌,因为有卢卡斯定理的存在,所以没有任何实际意义,可以不用留意。
 
 
 
接下来就是今天的正文了!!
先讲一则笑话放放松。
一天,狗熊在森林里便便,看到一只小白兔,问:“小白兔,你掉毛吗?”
小白兔说:“不掉。”
然后狗熊就拿起小白兔擦屁股。
第二天,狗熊在森林里吃东西,看到一只小松鼠,问:“小松鼠,你掉毛吗?”
小松鼠说:“不掉。”
然后狗熊就拿起小松鼠擦嘴。
“可是我是小白兔啊。”
 
咳咳,围观记得戴口罩,正文来了。

卢卡斯定理:要求 p p p为素数,最坏情况 o ( l o g n ) o(logn) o(logn)

考虑公式 C n k = n ! k ! ( n − k ) ! C_{n}^{k}=\frac{n!}{k!(n-k)!} Cnk=k!(nk)!n!
所以我们只要预处理出 1 1 1 n n n的阶乘,对 p p p取模。再预处理出 1 1 1 n n n的阶乘模p意义下的逆元,就可以利用 n ! ∗ i n v ( k ! ) ∗ i n v ( ( n − k ) ! ) n!*inv(k!)*inv((n-k)!) n!inv(k!)inv((nk)!) o ( 1 ) o(1) o(1)求组合数了。
但是注意一个问题,这个方法的前提是要有逆元。
p p p为合数的时候,不管 n n n范围是多少,都有可能出现逆元不存在的情况,十分麻烦,所以我们先解决素数的情况。
p p p是素数且 n ⩾ p n\geqslant p np时,就可能出现逆元不存在的情况。(至少 n < p n

n<p的情况可以求了)
那么考虑当 p p p为素数的时候怎么求,这就是卢卡斯定理了。
卢卡斯定理: C n k ≡ C n / p k / p × C n % p k % p ( m o d    p ) C_{n}^{k}\equiv C_{n/p}^{k/p}\times C_{n\%p}^{k\%p}(mod\;p ) CnkCn/pk/p×Cn%pk%p(modp),其中 p p p为素数。
然后就可以解决 n ⩾ p n\geqslant p np的情况了,简洁板书,就不证明了,读者可以自证或者查阅资料。
有了这个定理之后,我们只要预处理出 p − 1 p-1 p1的阶乘,就可以求解 n n n很大的情况。而对于素数 p p p很大的时候, n n n比较小,我们只要预处理到 n n n的阶乘就够了,这种情况甚至不需要卢卡斯定理。
模板:

#include 
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;
}

 
 

扩展卢卡斯定理:不限制 p p p的范围,复杂度 o ( p ) o(p) o(p)

光有卢卡斯定理,对于 p p p不为素数的情况,仍然无法求解,于是,就有了扩展卢卡斯定理
这个方法可以在 o ( p ) o(p) o(p)的时间下处理出组合数。
但是和卢卡斯定理又是截然不同的两种方法。

我们将模数 p p p用唯一分解定理展开成 p = p 1 k 1 … p n k n p=p_{1}^{k_{1}}…p_{n}^{k_{n}} p=p1k1pnkn,用素数幂的乘积的形式表示。
然后我们只要分别求解 { C n k ( m o d    p 1 k 1 ) C n k ( m o d    p 2 k 2 ) … C n k ( m o d    p n k n ) \left\{\begin{matrix} C_{n}^{k}(mod\;p_{1}^{k_{1}}) & & \\ C_{n}^{k}(mod\;p_{2}^{k_{2}}) & & \\ … & & \\ C_{n}^{k}(mod\;p_{n}^{k_{n}}) & & \end{matrix}\right. Cnk(modp1k1)Cnk(modp2k2)Cnk(modpnkn)的值,就可以用中国剩余定理求解了。
所以我们现在的问题就变成了如何求解 C n k ( m o d    p 1 k 1 ) C_{n}^{k}(mod\;p_{1}^{k_{1}}) Cnk(modp1k1)的值
还是考虑组合数公式: C n k = n ! k ! ( n − k ) ! C_{n}^{k}=\frac{n!}{k!(n-k)!} Cnk=k!(nk)!n!
但是分母可能存在因子 p 1 p_{1} p1,这样就不能求逆元了。所以我们把阶乘中的因子 p 1 p_{1} p1都提取出来。
就变成了 f ( n ! ) f ( ( n − k ) ! ) f ( k ! ) × p 1 w 1 − w 2 − w 3 \frac{f(n!)}{f((n-k)!)f(k!)}\times p_{1}^{w_{1}-w_{2}-w_{3}} f((nk)!)f(k!)f(n!)×p1w1w2w3。其中 f ( n ! ) f(n!) f(n!)表示去掉因子 p 1 p_{1} p1后模 p 1 k 1 p_{1}^{k1} p1k1的值, w 1 , 2 , 3 w_{1,2,3} w1,2,3表示从 3 3 3个阶乘中提取出来的因子 p 1 p_{1} p1的个数。
这样左边那个分母就可以求逆元了。
所以现在还有两个问题:

1. 如何求解 f ( n ! ) ( m o d    p 1 k ) f(n!)(mod\;p_{1}^{k}) f(n!)(modp1k)
2. 如果计算 w i w_{i} wi

第二个问题比较简单,如何求解 n ! n! n!阶乘里有多少个因子 p p p
1 1 1 n n n里能整除 p p p的有 ⌊ n p ⌋ \left \lfloor \frac{n}{p}\right \rfloor pn个。
1 1 1 n n n里能整除 p 2 p^{2} p2的有 ⌊ n p 2 ⌋ \left \lfloor \frac{n}{p^{2}}\right \rfloor p2n个。
              … …
1 1 1 n n n里能整除 p i p^{i} pi的有 ⌊ n p i ⌋ \left \lfloor \frac{n}{p^{i}}\right \rfloor pin个。
直到 p i > n p^{i}>n pi>n为止,然后把所有答案加起来就好了,复杂度是 l o g ( n ) log(n) log(n)的。
代码也很短。

ll getNumOfP(ll n,ll p)//返回n的阶乘里有多少个因子p
{
    if(n<p)return 0;
    return getNumOfP(n/p,p)+n/p;
}

然后最难解决的是第一个问题。
我们考虑 15 15 15的阶乘,模 3 2 3^{2} 32
1 × 2 × 3 × 4 × 5 × 6 × 7 × 8 × 9 × 10 × 11 × 12 × 13 × 14 × 15 1\times2\times3\times4\times5\times6\times7\times8\times9\times10\times11\times12\times13\times14\times15 1×2×3×4×5×6×7×8×9×10×11×12×13×14×15
我们把所有 3 3 3的倍数提取出来。
( 3 × 6 × 9 × 12 × 15 ) × ( 1 × 2 × 4 × 5 × 7 × 8 × 10 × 11 × 13 × 14 ) (3\times6\times9\times12\times15)\times(1\times2\times4\times5\times7\times8\times10\times11\times13\times14) (3×6×9×12×15)×(1×2×4×5×7×8×10×11×13×14)
再把左边的因子三各拿一个出来。
3 5 × ( 1 × 2 × 3 × 4 × 5 ) × ( 1 × 2 × 4 × 5 × 7 × 8 × 10 × 11 × 13 × 14 ) 3^{5}\times(1\times2\times3\times4\times5)\times(1\times2\times4\times5\times7\times8\times10\times11\times13\times14) 35×(1×2×3×4×5)×(1×2×4×5×7×8×10×11×13×14)
然后可以看到, 3 5 3^{5} 35是我们不要的,因为要算去除 3 3 3的因子。然后 ( 1 × 2 × 3 × 4 × 5 ) (1\times2\times3\times4\times5) (1×2×3×4×5) 5 5 5的阶乘,其实就是 ⌊ n p ⌋ \left \lfloor \frac{n}{p}\right \rfloor pn来的,我们可以递归求解。
然后是最后一个部分,如果不去掉3的倍数的话, 1 × 2 × … × 8 1\times2\times…\times8 1×2××8 10 × 11 × … × 17 10\times11\times…\times17 10×11××17 9 9 9下是同余的,去掉3的倍数之后发现依然是这样,这就有了一个循环节。所以我们只要遍历一遍 ( p − 1 ) (p-1) (p1),就可以求出来了,这个算法主要的复杂度就在这里,如果要优化的话也是突破点,如果还能优化的话

最后的模板:

#include 
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;
}

你可能感兴趣的:(数论)