排列组合学习笔记

排列与组合

定义

  • 组合数

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)\binom{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}\times 1,所以由加法原理得,nn个元素里面选mm个元素的方案数为Cn1m+Cm1n1C_{n-1}^m+C^{n-1}_{m-1}

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

性质5. Cnm=Cnm1×nm+1mC_n^m=C_n^{m-1}\times \frac{n-m+1}{m},由定义式相除得CnmCnm1=nm+1m\frac{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\times P_m^m,这里这个公式将组合与排列联系起来了,我们可以这样理解,排列就是先选mm个元素出来的方案数CnmC_n^m,每种方案的mm个元素再进行排列方案数PmmP_m^m,所以总方案数根据乘法原理得Cnm×PmmC_n^m\times 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!\times n_2!\times n_3!\cdots n_k!\times ans = n!,所以公式为multiPkk=n!n1!×n2!nk!multiP_k^k=\frac{n!}{n_1!\times 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_3\cdots +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^0\sim C_n^n内的n2n^2个组合数,我们使用杨辉三角的递推式即可,复杂度O(n2)O(n^2)(其实跑不满,严格来说是O(n×(n+1)2)O(\frac{n\times (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^0\sim 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}}\times C_{n\mod p}^{m\mod 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\n",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\n",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\n",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;
posted @ 2018-09-20 21:25  VictoryCzt  阅读(686)  评论(0编辑  收藏  举报