求组合数的四种方法

求组合数

公式法

  • 公式
    \(C_a^b=C_{a-1}^{b-1}+C_{a-1}^{b}\)
  • 公式理解
    在b个要抽取的物品中任选一个x物品,第一次抽取情况无非两种,抽到了x和没有抽到x,如果抽到了x那么就只需要在剩下的a-1个中再抽取b-1个即可,如果没有抽到x,就在剩下的a-1个中抽取b个,两次相加即为抽取的总种数。
  • 代码思路及实现
    我们只需要将数据范围n内的所有组合数都预处理出来,就可以实现求任一个组合数。时间复杂度\(o(n^2)\),本质是一个从1到n的等差数列求和。
    给定n组数,每组包含一对a,b,求每一组\(C_a^b\)
#include<iostream>
using namespace std;
const int N=2010;
long long c[N][N];
const int mod=1e9+7;
int main()
{
    int n;
    cin>>n;
    for(int i=0;i<=2000;i++)
    {
        for(int j=0;j<=i;j++)
        {
            if(j==0)
            {
                c[i][j]=1;
            }
            else
            {
                c[i][j]=(c[i-1][j]+c[i-1][j-1])%mod;
            }
        }
    }
    while(n--)
    {
        int a,b;
        scanf("%d %d",&a,&b);
        printf("%ld\n",c[a][b]);
    }
    
    
    return 0;
}
  • 代码细节
    注意当b=0时的边界判定

定义法

  • 算法思路
    由组合数定义\(C_a^b=\frac{a(a-1)(a-2)\dots(a-b+1)}{b!}=\frac{a!}{b!(a-b)!}\),这种结果一般会很大所以会要求模上一个数,所以我们算出a!后就不去算除法而是用逆元来算。
  • 代码实现
    \(0\le b\le a\le{10}^5\)如果 用公式法时间复杂度\(o(n^2)\)会超时,而定义法时间复杂度为\(o(nlogn)\)不会超时。
#include<iostream>
using namespace std;
const int mod=1e9+7;
const int N=1e5+10;
typedef long long LL;
LL fant[N],infant[N];//fant[i]代表i的阶乘,infant[i]代表i的阶乘的逆元
LL quick_mod(int a,int b,int p)//这道题mod是一个质数所以可以用快速幂来求
{
   LL res=1;
   while(b)
   {
       if(b&1)
       {
           res=res*a%p;
       }
       b>>=1;
       a=(LL)a*a%p;
   }
   return res;
}
int main()
{
   int n;
   cin>>n;
   fant[0]=infant[0]=1;
   for(int i=1;i<N;i++)
   {
       fant[i]=(LL)fant[i-1]*i%mod;
       infant[i]=(LL)infant[i-1]*quick_mod(i,mod-2,mod)%mod;//第29行
   }
   while(n--)
   {
       int a,b;
       scanf("%d %d",&a,&b);
       printf("%lld\n",(LL)fant[a]*infant[b]%mod*infant[a-b]%mod);
   }
   
   
   return 0;
}
  • 代码细节
    第29行求逆元的过程本质上就是\(\frac{a}{bc}\equiv a*b^{-1}*c^{-1}(mod\enspace p)\),即除以两个数同余先乘上一个数的逆元再乘上后一个数的逆元。

卢卡斯定理

  • 定理
    \(C_a^b\equiv C_{a\space mod\space p}^{b\space mod\space p}*C_{a/p}^{b/p}(mod\enspace p)\)
  • 数学推导
    要用到一个公式\((1+x)^p\equiv 1+x^p(mod\enspace p)\),这个公式的证明把左边展开即可:\((1+x)^p=C_p^0+C_p^1x+C_P^2x^2+\dots+C_p^px^p\)因为从\(C_p^1x\)\(c_p^{p-1}x^{p-1}\)都是p的倍数[1]模上一个p之后都为0,所以\((1+x)^p\equiv 1+x^p(mod\enspace p)\)
    将a和b用p进制表示为:
    \(a=a_kp^k+a_{k-1}p^{k-1}+a_{k-2}p^{k-2}+\dots+a_0p^0\)
    \(b=b_kp^k+b_{k-1}p^{k-1}+b_{k-2}p^{k-2}+\dots+b_0p^0\)
    对于\((1+x)^a\)\((1+x)^a=(1+x)^{a_kp^k+a_{k-1}p^{k-1}+a_{k-2}p^{k-2}+\dots+a_0p^0}=(1+x)^{a_kp^k}(1+x)^{a_{k-1}p^{k-1}}\dots(1+x)^{a_0p_0}=(1+x^{p^k})^{a_k}(1+x^{p^{k-1}})^{a_{k-1}}\dots(1+x^{p^0})^{a_0}\),由等式左边可以看出\(x^b\)的系数为\(C_a^b\),而\(x^b=x^{b_kp^k+b_{k-1}p^{k-1}+b_{k-2}p^{k-2}+\dots+b_0p^0}=(1+x)^{b_kp^k}(1+x)^{b_{k-1}p^{k-1}}\dots(1+x)^{b_0p_0}\),而从等式右边可以看出\(x^{b_kp^k}\)的系数为\(C_{a_k}^{b_k}\)以此类推得\(x^b\)的系数\(C_a^b\)\(=C_{a_k}^{b_k}C_{a_{k-1}}^{b_{k-1}}\dots C_{a_0}^{b_0}(mod\enspace p)\),因为a,b是用p进制表示的则有:\(a_0=a\space mod\space p,b_0=b\space mod\space p,\\ \lfloor a/p \rfloor\)实际意义为a在p进制下向右移一位,\(\lfloor b/p \rfloor\)实际意义为b在p进制下向右移一位。那么对\(C_{\lfloor a/p \rfloor}^{\lfloor b/p \rfloor}\)再进行一次上面的操作可以得到\(C_{\lfloor a/p \rfloor}^{\lfloor b/p \rfloor}=C_{a_k}^{b_k}C_{a_{k-1}}^{b_{k-1}}\dots C_{a_1}^{b_1}(mod\enspace p)\)
    综上有:\(C_a^b=C_{\lfloor a/p \rfloor}^{\lfloor b/p \rfloor}*C_{a\space mod\space p}^{a\space mod\space p}(mod\enspace p)\)

-代码实现
卢卡斯算法时间复杂度对于\(0\le b\le a\le {10}^{18},0\le p \le{10}^5\)\(o(klog_pn*n*log_2p)\)这里的数据组数k一般比较小

#include<iostream>
#include<algorithm>

using namespace std;
typedef long long LL;
int n;
int qmi(int a,int b,int p)
{
    int res=1;
    while(b)
    {
        if(b&1)
        {
            res=(LL)res*a%p;
        }
        b>>=1;
        a=(LL)a*a%p;
    }
    return res;
}
int C(LL a,LL b,int p)
{
    int res=1;
    for(int i=1,j=a;i<=b;i++,j--)
    {
        res=(LL)res*j%p;
        res=(LL)res*qmi(i,p-2,p)%p;
    }
    return res;
}
int lucas(LL a,LL b,int p)
{
    if(b>a)
    {
        return 0;
    }
    if(a<p)
    {
        return C(a,b,p);
    }
    return (LL)C(a%p,b%p,p)*lucas(a/p,b/p,p)%p;//a%p和b%p一定小于p所以直接用C来算组合数即可
}
int main()
{
    cin>>n;
    while(n--)
    {
        LL a,b;
        int p;
        scanf("%lld %lld %d",&a,&b,&p);
        printf("%d\n",lucas(a,b,p));
    }
    return 0;
}
  • 代码细节
    当a,b都小于p时候才用C函数来算,其余都继续递归直到小于p。

大精度计算

  • 针对类型
    可计算a,b较大且结果不模上一个数的组合数
  • 算法思路
    \(C_a^b=\frac{a!}{b!(a-b)!}\)对其分解质因数可得\(C_a^b=\frac{a!}{b!(a-b)!}=p_1^{\alpha_1}p_2^{\alpha_2}\dots p_n^{\alpha_n}(p_1p_2\dots p_n均为质数)\)对于任一个\(p_i\)它的次数都等于分子\(a!\)所含\(p_i\)的个数减去分母\(b!*(a-b)!\)所含的\(p_i\)的个数,求一个阶乘的某个质因子的个数有公式:\(cnt(a!)=\lfloor \frac{a}{p} \rfloor+\lfloor \frac{a}{p^2} \rfloor+\dots\)[2]一直加到分母比a大时停止(因为分母比a大时向下取整为0加了和没加一样),则结果为\(C_a^b=\prod \limits_{i=1}^{n}p_i^{\alpha_i}\)
  • 实现步骤
    先筛出2-a的所有质数,然后算出每个质数对应的次数,再算出\(\prod \limits_{i=1}^{n}p_i^{\alpha_i}\)即可
  • 代码实现
    输入a,b求出\(C_a^b(0\le b\le a\le 5000)\)
#include<iostream>
#include<algorithm>
#include<vector>

using namespace std;
typedef long long LL;
const int N=5010;
int primes[N];
int sum[N];
int cnt;
bool str[N];
int get(int x,int p)
{
    int res=0;
    while(x)
    {
        res+=x/p;
        x/=p;
    }
    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)//这里得写成while,当b大于10的时候t在这里就可能大于10从而需要多次进位
    {
        c.push_back(t%10);
        t/=10;
    }
}
void get_primes(int n)
    return c;
{
    for(int i=2;i<=n;i++)
    {
        if(!str[i])
        {
            primes[cnt++]=i;
        }
        for(int j=0;i<=n/primes[j];j++)
        {
            str[i*primes[j]]=true;
            if(i%primes[j]==0)
            {
                break;
            }
        }
    }
}
int main()
{
    int a,b;
    cin>>a>>b;
    get_primes(a);
    for(int i=0;i<cnt;i++)
    {
        int p=primes[i];
        sum[p]=get(a,p)-get(b,p)-get(a-b,p);
    }
    vector<int> res;
    res.push_back(1);
    for(int i=0;i<cnt;i++)
    {
        for(int j=0;j<sum[primes[i]];j++)
        {
            res=mul(res,primes[i]);
        }
    }
    for(int i=res.size()-1;i>=0;i--)
    {
        printf("%d",res[i]);
    }
    return 0;
}

-代码细节
注意求大精度数相乘的模板


  1. 当p为质数时,有公式:\(C_p^b\equiv 0(mod\enspace p)\)证明:\(C_p^b=\frac{p!}{b!(p-b)!}=\frac{p(p-1)!}{b(b-1)!(p-b)!}=\frac{p}{b}*C_{p-1}^{b-1}\)由于p是质数,则b与p互质,b在模p条件下存在逆元,则有:\(C_p^b=p*infact(b)*C_{p-1}^{b-1}(mod\enspace p)\)显然是一个P的倍数则模上p后为0. ↩︎

  2. \(\lfloor \frac{a}{p} \rfloor\)就表示a!中的a项乘积中是p的倍数的个数即a项乘积中能提出的p的个数,同理\(\lfloor \frac{a}{p^2} \rfloor\)就表示a!中的a项乘积中是\(p^2\)的倍数的个数即a项乘积中能提出的\(p^2\)的个数,以此类推就可以得到a!中含p的个数,这里你可能会有疑问,不应该加上二倍\(p^2\)的个数,三倍\(p^3\)的个数...才是所含p的个数吗,其实加上一倍就可以了,因为比如\(p^2\)的倍数的个数在算p的倍数的时候就已经被算过了,毕竟\(p^2\)的k倍数一定是\(p\)的k*p倍数。 ↩︎

posted @ 2023-07-07 20:21  Taco_gu  阅读(310)  评论(0)    收藏  举报