zoukankan      html  css  js  c++  java
  • 排列组合学习笔记

    排列与组合

    定义

    • 组合数

    nn个不同元素中,任取m(mn)m(m≤n)个元素并成一组,叫做从nn个不同元素中取出mm个元素的一个组合;从nn个不同元素中取出m(mn)m(m≤n)个元素的所有组合的个数,叫做从nn个不同元素中取出mm个元素的组合数。

    我们通常用大写字母CC表示,计算公式如下:
    Cnm=n!(nm)!m! C_n^m=frac{n!}{(n-m)!m!}
    CnmC_n^m也可以写作(nm)inom{n}{m}C(n,m)C(n,m)

    • 排列数

    排列,一般地,从nn个不同元素中取出mmnm(m≤n)个元素,按照一定的顺序排成一列,叫做从nn个元素中取出m个元素的一个排列(permutation)(permutation)。特别地,当m=nm=n时,这个排列被称作全排列(all permutation)(all permutation)

    我们通常用大写字母PP表示,计算公式如下:
    Pnm=n!(nm)! P_n^m=frac{n!}{(n-m)!}

    PnmP_n^m也可写作nPm_nP_m或者AnmA_n^m

    一些性质

    • 组合数

    性质1. 当m=0m=0或者n=mn=m时它的值为11。特别的,当m>nm>n时它的值为00。(这应该十分显然吧

    性质2. Cnm=CnnmC_n^m=C_n^{n-m},因为选mm个元素出来相当于你剩下mm个元素而选nmn-m个元素出来,所以方案数是一样的。

    性质3. Cnm=Cn1m+Cm1n1C_n^m=C_{n-1}^m+C^{n-1}_{m-1},这个就是组合数的一个递推式,它也是著名的杨辉三角,简单证明如下:

    nn个元素里面选mm个元素有两种选法,第一种为在前n1n-1个内选mm个,而不选第nn个,方案数为Cn1mC_{n-1}^m,或者在前n1n-1个元素内,只选m1m-1个,而第mm个选第nn个元素,方案数为Cn1m1×1C_{n-1}^{m-1} imes 1,所以由加法原理得,nn个元素里面选mm个元素的方案数为Cn1m+Cm1n1C_{n-1}^m+C^{n-1}_{m-1}

    性质4. (a+b)n=m=0nCnmanmbm(a+b)^n=sumlimits_{m=0}^{n}C_n^ma^{n-m}b^m,也就是二项式展开的系数。

    性质5. Cnm=Cnm1×nm+1mC_n^m=C_n^{m-1} imes frac{n-m+1}{m},由定义式相除得CnmCnm1=nm+1mfrac{C_n^m}{C_n^{m-1}}=frac{n-m+1}{m}

    • 排列数

    性质1. Pnn=n!P_n^n=n!,显然。

    性质2. Pnm=Cnm×PmmP_n^m=C_n^m imes P_m^m,这里这个公式将组合与排列联系起来了,我们可以这样理解,排列就是先选mm个元素出来的方案数CnmC_n^m,每种方案的mm个元素再进行排列方案数PmmP_m^m,所以总方案数根据乘法原理得Cnm×PmmC_n^m imes P_m^m

    • 补充
      有重复元素的全排列

    元素个数无限制:重复排列(permutationwithrepetiton)(permutationwith repetiton)是一种特殊的排列。从nn个不同元素中可重复地选取mm个元素。按照一定的顺序排成一列,称作从nn个元素中取mm个元素的可重复排列。当且仅当所取的元素相同,且元素的排列顺序也相同,则两个排列相同。由分步记数原理易知,从n个元素中取m个元素的可重复排列的不同排列数为nmn^m

    元素个数有限制:先有kk种元素,第ii种元素的个数为nin_i个,我们令n=i=1knin=sum_{i=1}^kn_i。我们可以先对元素编号,使其变成无重复元素,那么总方案数为n!n!,根据乘法原理可知,我们令答案为ansans,那么n1!×n2!×n3!nk!×ans=n!n_1! imes n_2! imes n_3!cdots n_k! imes ans = n!,所以公式为multiPkk=n!n1!×n2!nk!multiP_k^k=frac{n!}{n_1! imes n_2!cdots n_k!}

    可重复选择组合:设第ii个元素选xix_i个,转化为求方程x1+x2++xn=mx_1+x_2+cdots +x_n=m的非负整数解的个数,我们转化一下,令yi=xi+1y_i=x_i+1,那么原方程就等于求y1+y2+y3+yn=m+ny_1+y_2+y_3cdots +y_n=m+n的正整数解的个数,我们将其转化为n+mn+m11,然后使用插板法,将其插入n1n-1个板子,每个间隔里11的个数便是一个yiy_i的值,那么原问题转化为在n+m1n+m-1个位置上放n1n-1个板子的方案数,且板子之间至少有一个间隔,那么答案就显然为Cn+m1n1C_{n+m-1}^{n-1},也就是Cn+m1mC_{n+m-1}^{m}


    程序上的实现

    • 排列数

    对于全排列,我们直接O(n)O(n)求阶乘,取模或者高精即可。对于重复元素全排列,我们使用快速幂即可在O(log2m)O(log_2m)时间内解决。

    对于普通的排列,我们直接套用公式,预处理nn以内的阶乘,然后n!n!(nm)!(n-m)!相除即可。若取模,求逆元相乘即可。

    其余的情况求法类似。

    • 组合数

    第一种. 对于求一个组合数我们按照公式模拟,相除或者乘逆元即可,复杂度O(max(n,m))O(max(n,m))

    第二种. 对于求C10CnnC_1^0sim C_n^n内的n2n^2个组合数,我们使用杨辉三角的递推式即可,复杂度O(n2)O(n^2)(其实跑不满,严格来说是O(n×(n+1)2)O(frac{n imes (n+1)}{2})

    code

    C[0][0]=1;
    for(int i=1;i<=n;i++){
    	C[i][0]=C[i][1]=1;
    	for(int j=1;j<i;j++){
    	   C[i][j]=C[i-1][j]+C[i-1][j-1];
       }
    } 
    

    第三种. 对于求C10CnnC_1^0sim C_n^n内的n2n^2个组合数,我们预处理nn内的阶乘(若取模还要处理nn内阶乘的逆元),复杂度O(n)O(n)(加上逆元O(log2p+n)O(log_2p+n),其中pp为模数),每次求取时候用阶乘计算一下即可。

    第四种. 对于取模的情况下,模数pp较小而n,mn,m十分大,我们用LucasLucas定理(若模数不为质数则使用ExLucasExLucas定理扩展卢卡斯定理),公式Cnm=Cnpmp×Cnmod&ThinSpace;&ThinSpace;pmmod&ThinSpace;&ThinSpace;pC_n^m=C_{frac{n}{p}}^{frac{m}{p}} imes C_{nmod p}^{mmod p}。(具体证明与推导博主后期会另外总结_(¦3」∠)_)

    lucas的code
    luogu P3807
    不预处理
    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #define ll long long
    using namespace std;
    const int M=1e5+10;
    ll inv(ll a,ll b,ll mod){
      ll ans=1ll;
      for(;b;b>>=1,(a*=a)%=mod){
        if(b&1)(ans*=a)%=mod;
      }
      return ans;
    }
    ll calc(int a,int b,int p){
      if(a<=b) return a==b;
      if(b>a-b) b=a-b;
      ll ans=1ll,c1=1ll,c2=1ll;
      for(ll i=0;i<b;i++){
        c1=(c1*(a-i))%p;
        c2=(c2*(b-i))%p;
      }
      ans=(c1*inv(c2,p-2,p))%p;
      return ans;
    }
    ll lucas(int n,int m,int p){
    
      ll ans=1ll;
      for(;n&&m&&ans;n/=p,m/=p){
        ans=(ans*calc(n%p,m%p,p))%p;
      }
      return ans;
    }
    int T,n,m,p;
    int main(){
      scanf("%d",&T);
      while(T--){
        scanf("%d%d%d",&n,&m,&p);
        printf("%lld
    ",lucas(n+m,n,p));
      }
      return 0;
    }
    用时: 0ms / 内存: 1746KB
    /*******************************************/
    预处理版本
    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #define ll long long
    using namespace std;
    const int M=1e5+10;
    ll ny[M],pow[M];
    int n,m,p,T;
    ll lucas(int a,int b){
        if(a<b) return 0;
        if(a<p) return pow[a]*ny[b]*ny[a-b]%p;
        return lucas(a/p,b/p)*lucas(a%p,b%p)%p;
    }
    int main()
    {
        scanf("%d",&T);
        while(T--){
            scanf("%d%d%d",&n,&m,&p);
            ny[0]=ny[1]=pow[0]=pow[1]=1ll;
            for(int i=2;i<=n+m;i++) pow[i]=1ll*pow[i-1]*i%p;
            for(int i=2;i<=n+m;i++) ny[i]=1ll*(p-p/i)*ny[p%i]%p;
            for(int i=2;i<=n+m;i++) ny[i]=1ll*ny[i-1]*ny[i]%p;
            printf("%lld
    ",lucas(n+m,m));
        }
        return 0;
    }
    用时: 76ms / 内存: 2558KB
    

    下面给出扩展lucas的模板代码

    分解质因数+中国剩余定理+lucas定理
    luogu P4720
    用时: 88ms / 内存: 1746KB
    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #define ll long long 
    using namespace std;
    ll exgcd(ll a,ll b,ll &x,ll &y){
        if(!b){x=1;y=0;return a;}
        else{ll now=exgcd(b,a%b,y,x);y-=x*(a/b);return now;}
    }
    ll fpow(ll a,ll b,ll mod){
        ll ans=1;
        for(;b;b>>=1,a=(a*a)%mod){
            if(b&1) ans=(ans*a)%mod;
        }
        return ans%mod;
    }
    ll inv(ll a,ll mod){
        if(!a) return 0;
        ll x=0,y=0;
        exgcd(a,mod,x,y);
        x=((x%mod)+mod)%mod;
        if(!x) x=mod;
        return x;
    }
    ll calc(ll n,ll pi,ll pk){
        if(!n) return 1;
        ll ans=1;
        if(n/pk){
            for(ll i=2;i<=pk;i++)
               if(i%pi) ans=(ans*i)%pk;
            ans=fpow(ans,n/pk,pk);   
        }
        for(ll i=2,up=n%pk;i<=up;i++)
            if(i%pi) ans=(ans*i)%pk;
        return ans*calc(n/pi,pi,pk)%pk;
    }
    ll C(ll n,ll m,ll mod,ll pi,ll pk){
        if(m>n) return 0;
        ll a=calc(n,pi,pk),b=calc(m,pi,pk),c=calc(n-m,pi,pk);
        ll k=0,ans;
        for(ll i=n;i;i/=pi) k+=i/pi;
        for(ll i=m;i;i/=pi) k-=i/pi;
        for(ll i=n-m;i;i/=pi) k-=i/pi;
        ans=a*inv(b,pk)%pk*inv(c,pk)%pk*fpow(pi,k,pk)%pk;
        return ans*(mod/pk)%mod*inv(mod/pk,pk)%mod;
    }
    
    ll crt(ll n,ll m,ll mod){
        ll ans=0;
        for(ll x=mod,i=2;i<=mod;i++){
            if(x%i==0){
                ll pk=1;
                while(x%i==0) pk*=i,x/=i;
                ans=(ans+C(n,m,mod,i,pk))%mod;
            }
        }
        return ans;
    }
    
    ll n,m,p;
    int main(){
        scanf("%lld%lld%lld",&n,&m,&p);
        printf("%lld
    ",crt(n,m,p)%p);
        return 0;
    }
    

    第五种. 对于只求nn一定的,而mm变化的,可以使用组合数的性质5递推即可,复杂度为O(n)O(n),取模的话,预处理逆元即可,复杂度O(n+n)O(n+n)

    code

    C(n,m)
    C[1]=N;C[0]=1;
    inv[1]=1;//逆元
    for(long long i=2;i<min(N,Mod);i++) inv[i]=((Mod-Mod/i)*inv[Mod%i])%Mod;
    for(long long i=2,j=N-1;i<=M;i++,j--)C[i]=C[i-1]*j%Mod*inv[i])%Mod;
    

    预处理分数线上下两部分。

    code

    fac[0]=1;
    for(long long i=2;i<=N;i++)fac[i]=fac[i-1]*i%Mod;
    inv[N]=pow(fac[N],mod-2);
    for(long long i=N-1;i>=1;i++)inv[i]=inv[i+1]*(i+1)%Mod;
    
  • 相关阅读:
    C艹老师布置的思考题
    计蒜客练习题:网页跳转(java / C++仔细)
    计蒜客练习题:水果店(嵌套map)
    计蒜客练习题:蒜头君面试(map + max_element)
    小希的迷宫 HDU 1272(并查集)
    OpenJ_Bailian 1061
    Aizu 2224(并查集)
    Aizu 0189 (最短路)
    POJ 2377(最大生成树)
    OpenJ_Bailian 4118(dp)
  • 原文地址:https://www.cnblogs.com/VictoryCzt/p/10053421.html
Copyright © 2011-2022 走看看