求组合数(四种方法)

本文介绍了计算组合数的四种方法:递推(基于杨辉三角),快速幂结合乘法逆元,卢卡斯定理以及高精度计算。递推方法适用于较小规模的n和m,快速幂适合中等规模,卢卡斯定理用于大数但模一个小质数的情况,高精度计算则针对无需取模的大数组合数。每种方法都有其适用范围和时间复杂度。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

求组合数(四种方法)

文章首发于我的个人博客:欢迎大佬们来逛逛

递推(杨辉三角)

对于求一个数字的组合数: C n m C_{n}^{m} Cnm 可以分解为这两种形式:

  • C n − 1 m − 1 C_{n-1}^{m-1} Cn1m1 :选择当前数
  • C n − 1 m C_{n-1}^{m} Cn1m :不选择当前数

因此就得到了递推公式:

C n m = C n − 1 m − 1 + C n − 1 m C_{n}^{m}=C_{n-1}^{m-1}+C_{n-1}^{m} Cnm=Cn1m1+Cn1m

for (int i=0;i<=n;i++){
        for (int j=0;j<=i;j++){
            if (j==0){
                dp[i][j]=1; //n个数中取0个,默认为1
            }
            else{
                dp[i][j]=(dp[i-1][j]+dp[i-1][j-1])%p;
            }
        }
    }

由于需要用到二重循环,因此此方法适用的范围是: n , m < = 1 0 4 n,m<=10^4 n,m<=104 左右

时间复杂度: O ( n 2 ) O(n^2) O(n2)


快速幂+乘法逆元

考虑组合数的计算公式:

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

我们可以直接计算这个式子中的每一项。

费马小定理:如果 p p p 是一个质数,并且 a a a p p p 互质,则满足: a p − 1 ≡ 1 ( m o d   p ) a^{p-1} \equiv 1(mod \space p) ap11(mod p)

乘法逆元:如果 a a a p p p 都是质数,并且满足同余方程式: a ⋅ x ≡ 1 ( m o d   p ) a\cdot x \equiv 1(mod \space p) ax1(mod p) ,则称 x x x a a a p p p 的乘法逆元,即: 1 a   m o d   p = x \frac{1}{a\space mod\space p} =x a mod p1=x


那么我们可以将费马小定理进行转化,得到: a ⋅ a p − 2 ≡ 1 ( m o d   p ) a\cdot a^{p-2}\equiv 1(mod \space p) aap21(mod p)

则如果 a a a p p p 都是质数, x = a p − 2 x=a^{p-2} x=ap2 就是 a a a p p p 的乘法逆元。

因此我们要求 a a a p p p 的乘法逆元,直接计算 a p − 2 a^{p-2} ap2 即可


我们规定:

  • f f f 数组为阶乘数组,保存每个数字阶乘意义下的阶乘。
  • g g g 数组为逆元数组,保存每个数组阶乘意义下的逆元。

因此就可以转换为 C n m = f [ n ] ⋅ g [ n − m ] ⋅ g [ m ] C_{n}^{m} = {f[n]}\cdot g[n-m] \cdot g[m] Cnm=f[n]g[nm]g[m]

如何求这个数组的值呢?

  • 对于阶乘来说比较容易。
  • 对于逆元,需要进行转化: 1 i ! = 1 ( i − 1 ) ! ⋅ 1 i \frac{1}{i!} = \frac{1}{(i-1)!} \cdot \frac{1}{i} i!1=(i1)!1i1 ,因此就是 g [ i ] = g [ i − 1 ] ⋅ i n v ( i ) g[i]=g[i-1]\cdot inv(i) g[i]=g[i1]inv(i)
f[0]=g[0]=1; //初始值
for (int i=1;i<=n;i++){
    f[i]=f[i-1]*i%p; //计算i的阶乘
    g[i]=g[i-1]*qpow(i,p-2)%p; //计算i的乘法逆元 qpow为快速幂
}
...
int get(int n,int m){ //得到C(n,m)的组合数答案
    return f[n]*g[m]*g[n-m]%p;
}

容易得知此方法计算组合数的使用范围大概是: n , m < = 1 0 7 n,m<=10^7 n,m<=107 ,其主要限制为 f o r for for 循环和数组空间大小

时间复杂度: O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)


卢卡斯定理

此方法用于非常大的 n n n m m m ,但是 p p p 却比较小的计算。

定理:

C n m = C n p m p ⋅ C n % p m % p % p C_{n}^{m}=C_{\frac{n}{p}}^{\frac{m}{p}} \cdot C_{n\%p}^{m\% p}\%p Cnm=CpnpmCn%pm%p%p

即计算组合数就可以利用拆分的方法。(就不证明了)

然后注意到 p p p 是一个质数,因此 n n n m m m 对其取余后,最后的结果一定比 p p p

因此就可以对第二个式子进行 快速幂+逆元 的方法,我们知道这个方法的瓶颈是过大的 n n n 导致空间不够或者循环过大,通过这种方法缩小后便可以直接使用此方法。

我们便可以利用递归来依次缩小 n n n m m m 的值,进而计算部分组合数,最后相乘即可,递归结束的条件是 m = = 0 m==0 m==0 ,此时返回 1 即可。

注意在计算某个 C i j C_{i}^{j} Cij 的时候,我们的两个数组的预处理是到 p p p 为止(因为不可能比 p p p 还大了)的。

f[0]=g[0]=1; //初始值
for (int i=1;i<=p;i++){
    f[i]=f[i-1]*i%p; //计算i的阶乘
    g[i]=g[i-1]*qpow(i,p-2)%p; //计算i的乘法逆元 qpow为快速幂
}
...
int get(int n,int m){ //得到C(n,m)的组合数答案
    return f[n]*g[m]*g[n-m]%p;
}
...
int lucas(int n,int m){
    if (m==0){
        return 1;
    }
    return lucas(n/p,m/p)*get(n%p,m%p)%p;
}

此方法使用范围(大概): n , m < = 1 0 18 , p < = 1 0 6 n,m<=10^{18} ,p<=10^6 n,m<=1018p<=106

时间复杂度: O ( n ⋅ l o g 2 n ) O(n\cdot log_2n) O(nlog2n)


高精度组合数

如果我们需要求 C n m C_{n}^{m} Cnm 而不需要求其模数,我们可以发现这个数字将会变得非常大,因为组合数的增长是非常快的!此时我们必须要采用高精度的方法。

此方法我们仍然需要用到公式:

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

步骤:

  1. 首先预处理所有 [ 1 , n ] [1,n] [1,n] 范围内的质数,使用线氏筛。
  2. 然后统计某个数阶乘意义下能够包含质数的个数
    1. 先处理 n ! n! n! 能够包含某质数的个数
    2. 再处理 ( n − m ) ! (n-m)! (nm)! 能够包含某质数的个数
    3. 再处理 m ! m! m! 能够包含某质数的个数
    4. 最后一式减去二三两式就处理出了 C i j C_{i}^{j} Cij 所包含的某质数的个数
  3. 最后对所有的质数都执行相同的操作,然后用 C i j ⋅ p C_{i}^{j}\cdot p Cijp ,其中 p是质数。

其中 C i j ⋅ p C_{i}^{j}\cdot p Cijp 可以表示为:

∏ a b C [ i ] ⋅ p , p ∈ [ 1 , 包含的 p 的个数 ] \prod_{a}^{b}C[i]\cdot p, p\in[1,包含的p的个数] abC[i]p,p[1,包含的p的个数]

我们用高精度乘法来维护。

int get(int n,int p){
    //n!有多少p质数
    int cnt=0;
    while (n){
        cnt+=n/p;
        n/=p;
    }
    return cnt;
}
int how(int n,int m,int p){
    return get(n,p)-get(n-m,p)-get(m,p);
}
void mul(int s,int& len){
    //高精度乘法
    int t=0;
    for(int i=0;i<len;i++){
        t+=C[i]*s;
        C[i]=t%10;
        t/=10;
    }
    while (t){
        C[len++]=t%10;
        t/=10;
    }
}
signed main(){
    //求C(n,m)
    std::cin>>n>>m; 
    init();
    int len=1;
    C[0]=1;
    for (int i=1;i<=cnt;i++){
        int x=primes[i];
        int s=how(n,m,x); //C[i]中有多少质数x
        while (s--){
            mul(x,len);
        }
    }
    for (int i=0;i<len;i++){
        std::cout<<C[len-i-1];
    }
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Yuleo_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值