第三十三章 数论——组合数详解(2)

一、组合数——卢卡斯定理

1、问题

第三十三章 数论——组合数详解(2)_第1张图片
这道题中, a , b a,b a,b的范围都是很大的,我们就无法直接用到之前所讲解的预处理阶乘的方法。

如果大家没有看过作者写的组合数(1)的话,建议大家先去看一下,今天所讲的问题需要上一篇文章的铺垫,传送门:组合数(1)

而此时就需要用到我们后文介绍的卢卡斯定理了。

2、卢卡斯定理

(1)定理内容

C a b = C a / p b / p ∗ C a   m o d ( p ) b   m o d ( p ) C_a^b=C_{a/p}^{b/p}*C_{a\ mod(p)}^{{b\ mod(p)}} Cab=Ca/pb/pCa mod(p)b mod(p)

(2)定理证明

在证明这个算法之前,我们先证明几个小的结论做铺垫。

结论1:

0 < x < p 00<x<p的时候:
C p x ≡ 0   m o d ( p ) C_p^x\equiv 0\ mod(p) Cpx0 mod(p)

这个很好证明
C p x = p ! x ! ( p − x ) ! = p ( p − 1 ) ! x ( x − 1 ) ! ( p − x ) ! = p x ∗ C p − 1 x − 1 C_p^x=\frac{p!}{x!(p-x)!}=\frac{p(p-1)!}{x(x-1)!(p-x)!}=\frac{p}{x}*C_{p-1}^{x-1} Cpx=x!(px)!p!=x(x1)!(px)!p(p1)!=xpCp1x1

所以 C p x C_p^x Cpx p p p的倍数。所以 C p x ≡ 0   m o d ( p ) C_p^x\equiv 0\ mod(p) Cpx0 mod(p)

结论2:

( 1 + x ) p ≡ 1 + x p ( m o d   p ) (1+x)^p\equiv1+x^p(mod\ p) (1+x)p1+xp(mod p)

证明:
根据二项展开式:

( 1 + x ) p = C p 0 x 0 + C p 1 x 1 + C p 2 x 2 + . . . + C p p x p (1+x)^p=C_p^0x^0+C_p^1x^1+C_p^2x^2+...+C_p^px^p (1+x)p=Cp0x0+Cp1x1+Cp2x2+...+Cppxp

根据我们的结论1
我们中间的项在模p的时候,都等于0。

因此,我们可以化简一下就会得到我们的结论2:

( 1 + x ) p ≡ 1 + x p ( m o d   p ) (1+x)^p\equiv1+x^p(mod\ p) (1+x)p1+xp(mod p)

卢卡斯定理证明:

a = n p + d , b = m p + d a=np+d,b=mp+d a=np+d,b=mp+d

根据二项展开式:
( 1 + x ) a ≡ ∑ i = 0 a C a i x i ( m o d   p ) (1+x)^a\equiv \sum_{i=0}^{a}C_a^ix^i(mod\ p) (1+x)ai=0aCaixi(mod p)

我们将 a = n p + d a=np+d a=np+d带入:

( 1 + x ) a (1+x)^a (1+x)a
≡ ( 1 + x ) n p + d \equiv (1+x)^{np+d} (1+x)np+d
≡ ( ( 1 + x ) p ) n ( 1 + x ) d \equiv \big((1+x)^p\big)^n(1+x)^d ((1+x)p)n(1+x)d

根据结论2,我们可以继续化简得:

≡ ( 1 + x p ) n ( 1 + x ) d \equiv \big(1+x^p\big)^n(1+x)^d (1+xp)n(1+x)d

再根据二项式展开:

≡ ∑ i = 0 n C n i x i p ∗ ∑ j = 0 d C d j x j ( m o d   p ) \equiv \sum_{i=0}^{n}C_n^ix^{ip}*\sum_{j=0}^{d}C_d^jx^{j}(mod\ p) i=0nCnixipj=0dCdjxj(mod p)

所以我们就能得到:
∑ i = 0 a C a i x i ≡ ∑ i = 0 n C n i x i p ∗ ∑ j = 0 d C d j x j ( m o d   p ) \sum_{i=0}^{a}C_a^ix^i\equiv \sum_{i=0}^{n}C_n^ix^{ip}*\sum_{j=0}^{d}C_d^jx^{j}(mod\ p) i=0aCaixii=0nCnixipj=0dCdjxj(mod p)

左式和右式中,一一对应,我们可以得到如下结果:

对于任何一项: x e x^e xe

C a b x b = C n x ∗ C d y x b C_a^bx^b=C_n^{x}*C_d^{y}x^b Cabxb=CnxCdyxb

其中: b = x ∗ p + y b=x*p+y b=xp+y并且 a = n p + d a=np+d a=np+d

即:
C a b ≡ C n x ∗ C d y C_a^b\equiv C_n^{x}*C_d^{y} CabCnxCdy

而:

x = b p n = a p x=\frac{b}{p}\\ n=\frac{a}{p} x=pbn=pa

y = b   m o d   p d = a   m o d   p y=b\ mod\ p\\ d=a\ mod\ p y=b mod pd=a mod p

我们将其带入即可得到:
C a b = C a / p b / p ∗ C a   m o d ( p ) b   m o d ( p ) C_a^b=C_{a/p}^{b/p}*C_{a\ mod(p)}^{{b\ mod(p)}} Cab=Ca/pb/pCa mod(p)b mod(p)

上述式子即我们的卢卡斯定理

3、代码

#include
using namespace std;
typedef long long ll;
int n;
ll qmi(ll a,ll b,ll p)
{
    ll res=1;
    while(b)
    {
        if(b&1)res=res%p*a%p;
        a=a%p*a%p;
        b>>=1;
    }
    return res;
}
ll c(ll a,ll b, ll p)
{
    if(b>a)return 0;
    ll res=1;
    for(int i=1,j=a;i<=b;i++,j--)
    {
        res=res%p*j%p;
        res=res%p*qmi(i,p-2,p)%p;
    }
    return res;
}
ll lucas(ll a,ll b,ll p)
{
    if(a<p&&b<p)return c(a,b,p);
    else return c(a%p,b%p,p)*lucas(a/p,b/p,p)%p;
}
int main()
{
    cin>>n;
    while(n--)
    {
        ll a,b,p;
        cin>>a>>b>>p;
        cout<<lucas(a,b,p)<<endl;
    }
}

这里解释一下我们的中间计算 C a b C_a^b Cab的代码:

我们这里用到了另一个定义:

C a b = A a b A b b = a ( a − 1 ) . . . ( a − b + 1 ) b ( b − 1 ) ( b − 2 ) . . . 1 C_a^b=\frac{A_a^b}{A_b^b}=\frac{a(a-1)...(a-b+1)}{b(b-1)(b-2)...1} Cab=AbbAab=b(b1)(b2)...1a(a1)...(ab+1)

二、组合数——高精度+欧拉筛

1、问题:

第三十三章 数论——组合数详解(2)_第2张图片
这道题的关键就在于题目给出的 a a a b b b实在是太大了,肯定会爆掉。所以我们要使用高精度算法

2、思路

这道题的难点就是会爆掉,所以我们要用到最开始学习的高精度算法。然后利用定义求解。

我们先来回顾一下组合数的定义:

C n m = n ! m ! ( n − m ) ! C_n^m=\frac{n!}{m!(n-m)!} Cnm=m!(nm)!n!

如果我们直接套用高精度算法的话,肯定是相当麻烦的。

所以我们将其变形一下:

根据算数基本定理,我们能够将 C n m C_n^m Cnm写成有限个质数乘积的形式。

那么问题来了,我们如何拆解呢?

首先,拆解质数的关键,在于我们要知道可能包含哪些质数。由于我们的 n ≥ m n\geq m nm。所以我们只需要去筛选 1 − n 1-n 1n之间所有的质数即可。

而筛选质数的话,我们可以选择之前学过的埃氏筛法或者欧拉筛法。我们此处就选择欧拉筛法了。

当我们知道了包含的质数之后,我们还要计算的是 C n m C_n^m Cnm中所含质数的个数。

那么此时我们将用到下面这个结论:

n ! 中 p 的个数: s u m = n p + n p 2 + n p 3 + . . . . n!中p的个数:\\ sum=\frac{n}{p}+\frac{n}{p^2}+\frac{n}{p^3}+.... n!p的个数:sum=pn+p2n+p3n+....

为什么呢?
如果我们的阶乘中含有一个 p k p^k pk的倍数。那么此时这个数应该包含 k k k p p p

如果假设 k > 1 k>1 k>1的话,

那么他必定是 p p p的个数,此时它会被取出来一次。

同时,如果 k > 2 k>2 k>2的话,

那么它也必定是 p 2 p^2 p2的个数,此时它会被取出来一次。

那么以此类推,它就会被取出来 k k k次,也就是说它会被计算 k k k次。恰好验证了我们的公式。

当我计算出了质数和对应的指数的时候,我们只需要套用一下高精度乘法,乘在一起即可。

3、代码

#include
#include
using namespace std;
const int N=5e3+10;
int primes[N],cnt;
int sum[N];
bool st[N];
void get_primes(int a)
{
    for(int i=2;i<=a;i++)
    {
        if(!st[i])primes[cnt++]=i;
        for(int j=0;primes[j]<=a/i;j++)
        {
            st[primes[j]*i]=true;
            if(i%primes[j]==0)break;
        }
    }
}
int get_nums(int a,int pri)
{
    int res=0;
    while(a)
    {
        res+=a/pri;
        a/=pri;
    }
    return res;
}
vector<int>mul(vector<int>a,int b)
{
    vector<int>c;
    int t=0;
    for(int i=0;i<a.size();i++)
    {
        t+=a[i]*b;
        c.push_back(t%10);
        t/=10;
    }
    while(t)
    {
        c.push_back(t%10);
        t/=10;
    }
    return c;
}

int main()
{
    int a,b;
    cin>>a>>b;
    get_primes(a);
    for(int i=0;i<cnt;i++)
    {
        int p=primes[i];
        sum[i]=get_nums(a,p)-get_nums(b,p)-get_nums(a-b,p);
    }
    
    vector<int>res;
    res.push_back(1);
    
    for(int i=0;i<cnt;i++)
    {
        for(int j=0;j<sum[i];j++)
        {
            res=mul(res,primes[i]);
        }
    }
    for(int i=res.size()-1;i>=0;i--)printf("%d",res[i]);
    puts("");
    return 0;
}

你可能感兴趣的:(算法合集,c++,算法,图论)