排列与组合
定义
- 组合数
从 n n n个不同元素中,任取 m ( m ≤ n ) m(m≤n) m(m≤n)个元素并成一组,叫做从 n n n个不同元素中取出 m m m个元素的一个组合;从 n n n个不同元素中取出 m ( m ≤ n ) m(m≤n) m(m≤n)个元素的所有组合的个数,叫做从 n n n个不同元素中取出 m m m个元素的组合数。
我们通常用大写字母
C
C
C表示,计算公式如下:
C
n
m
=
n
!
(
n
−
m
)
!
m
!
C_n^m=\frac{n!}{(n-m)!m!}
Cnm=(n−m)!m!n!
C
n
m
C_n^m
Cnm也可以写作
(
n
m
)
\binom{n}{m}
(mn)或
C
(
n
,
m
)
C(n,m)
C(n,m)
- 排列数
排列,一般地,从 n n n个不同元素中取出 m ( m ≤ n ) m(m≤n) m(m≤n)个元素,按照一定的顺序排成一列,叫做从 n n n个元素中取出m个元素的一个排列 ( p e r m u t a t i o n ) (permutation) (permutation)。特别地,当 m = n m=n m=n时,这个排列被称作全排列 ( a l l p e r m u t a t i o n ) (all\ permutation) (all permutation)。
我们通常用大写字母
P
P
P表示,计算公式如下:
P
n
m
=
n
!
(
n
−
m
)
!
P_n^m=\frac{n!}{(n-m)!}
Pnm=(n−m)!n!
P n m P_n^m Pnm也可写作 n P m _nP_m nPm或者 A n m A_n^m Anm
一些性质
- 组合数
性质1. 当
m
=
0
m=0
m=0或者
n
=
m
n=m
n=m时它的值为
1
1
1。特别的,当
m
>
n
m>n
m>n时它的值为
0
0
0。(这应该十分显然吧)
性质2. C n m = C n n − m C_n^m=C_n^{n-m} Cnm=Cnn−m,因为选 m m m个元素出来相当于你剩下 m m m个元素而选 n − m n-m n−m个元素出来,所以方案数是一样的。
性质3. C n m = C n − 1 m + C m − 1 n − 1 C_n^m=C_{n-1}^m+C^{n-1}_{m-1} Cnm=Cn−1m+Cm−1n−1,这个就是组合数的一个递推式,它也是著名的杨辉三角,简单证明如下:
n n n个元素里面选 m m m个元素有两种选法,第一种为在前 n − 1 n-1 n−1个内选 m m m个,而不选第 n n n个,方案数为 C n − 1 m C_{n-1}^m Cn−1m,或者在前 n − 1 n-1 n−1个元素内,只选 m − 1 m-1 m−1个,而第 m m m个选第 n n n个元素,方案数为 C n − 1 m − 1 × 1 C_{n-1}^{m-1}\times 1 Cn−1m−1×1,所以由加法原理得, n n n个元素里面选 m m m个元素的方案数为 C n − 1 m + C m − 1 n − 1 C_{n-1}^m+C^{n-1}_{m-1} Cn−1m+Cm−1n−1。
性质4. ( a + b ) n = ∑ m = 0 n C n m a n − m b m (a+b)^n=\sum\limits_{m=0}^{n}C_n^ma^{n-m}b^m (a+b)n=m=0∑nCnman−mbm,也就是二项式展开的系数。
性质5. C n m = C n m − 1 × n − m + 1 m C_n^m=C_n^{m-1}\times \frac{n-m+1}{m} Cnm=Cnm−1×mn−m+1,由定义式相除得 C n m C n m − 1 = n − m + 1 m \frac{C_n^m}{C_n^{m-1}}=\frac{n-m+1}{m} Cnm−1Cnm=mn−m+1。
- 排列数
性质1. P n n = n ! P_n^n=n! Pnn=n!,显然。
性质2. P n m = C n m × P m m P_n^m=C_n^m\times P_m^m Pnm=Cnm×Pmm,这里这个公式将组合与排列联系起来了,我们可以这样理解,排列就是先选 m m m个元素出来的方案数 C n m C_n^m Cnm,每种方案的 m m m个元素再进行排列方案数 P m m P_m^m Pmm,所以总方案数根据乘法原理得 C n m × P m m C_n^m\times P_m^m Cnm×Pmm。
- 补充
有重复元素的全排列
元素个数无限制:重复排列 ( p e r m u t a t i o n w i t h r e p e t i t o n ) (permutationwith repetiton) (permutationwithrepetiton)是一种特殊的排列。从 n n n个不同元素中可重复地选取 m m m个元素。按照一定的顺序排成一列,称作从 n n n个元素中取 m m m个元素的可重复排列。当且仅当所取的元素相同,且元素的排列顺序也相同,则两个排列相同。由分步记数原理易知,从n个元素中取m个元素的可重复排列的不同排列数为 n m n^m nm。
元素个数有限制:先有 k k k种元素,第 i i i种元素的个数为 n i n_i ni个,我们令 n = ∑ i = 1 k n i n=\sum_{i=1}^kn_i n=∑i=1kni。我们可以先对元素编号,使其变成无重复元素,那么总方案数为 n ! n! n!,根据乘法原理可知,我们令答案为 a n s ans ans,那么 n 1 ! × n 2 ! × n 3 ! ⋯ n k ! × a n s = n ! n_1!\times n_2!\times n_3!\cdots n_k!\times ans = n! n1!×n2!×n3!⋯nk!×ans=n!,所以公式为 m u l t i P k k = n ! n 1 ! × n 2 ! ⋯ n k ! multiP_k^k=\frac{n!}{n_1!\times n_2!\cdots n_k!} multiPkk=n1!×n2!⋯nk!n!
可重复选择组合:设第 i i i个元素选 x i x_i xi个,转化为求方程 x 1 + x 2 + ⋯ + x n = m x_1+x_2+\cdots +x_n=m x1+x2+⋯+xn=m的非负整数解的个数,我们转化一下,令 y i = x i + 1 y_i=x_i+1 yi=xi+1,那么原方程就等于求 y 1 + y 2 + y 3 ⋯ + y n = m + n y_1+y_2+y_3\cdots +y_n=m+n y1+y2+y3⋯+yn=m+n的正整数解的个数,我们将其转化为 n + m n+m n+m个 1 1 1,然后使用插板法,将其插入 n − 1 n-1 n−1个板子,每个间隔里 1 1 1的个数便是一个 y i y_i yi的值,那么原问题转化为在 n + m − 1 n+m-1 n+m−1个位置上放 n − 1 n-1 n−1个板子的方案数,且板子之间至少有一个间隔,那么答案就显然为 C n + m − 1 n − 1 C_{n+m-1}^{n-1} Cn+m−1n−1,也就是 C n + m − 1 m C_{n+m-1}^{m} Cn+m−1m。
程序上的实现
- 排列数
对于全排列,我们直接 O ( n ) O(n) O(n)求阶乘,取模或者高精即可。对于重复元素全排列,我们使用快速幂即可在 O ( l o g 2 m ) O(log_2m) O(log2m)时间内解决。
对于普通的排列,我们直接套用公式,预处理 n n n以内的阶乘,然后 n ! n! n!和 ( n − m ) ! (n-m)! (n−m)!相除即可。若取模,求逆元相乘即可。
其余的情况求法类似。
- 组合数
第一种. 对于求一个组合数我们按照公式模拟,相除或者乘逆元即可,复杂度 O ( m a x ( n , m ) ) O(max(n,m)) O(max(n,m))。
第二种. 对于求 C 1 0 ∼ C n n C_1^0\sim C_n^n C10∼Cnn内的 n 2 n^2 n2个组合数,我们使用杨辉三角的递推式即可,复杂度 O ( n 2 ) O(n^2) O(n2)(其实跑不满,严格来说是 O ( n × ( n + 1 ) 2 ) O(\frac{n\times (n+1)}{2}) O(2n×(n+1)))
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];
}
}
第三种. 对于求 C 1 0 ∼ C n n C_1^0\sim C_n^n C10∼Cnn内的 n 2 n^2 n2个组合数,我们预处理 n n n内的阶乘(若取模还要处理 n n n内阶乘的逆元),复杂度 O ( n ) O(n) O(n)(加上逆元 O ( l o g 2 p + n ) O(log_2p+n) O(log2p+n),其中 p p p为模数),每次求取时候用阶乘计算一下即可。
第四种. 对于取模的情况下,模数
p
p
p较小而
n
,
m
n,m
n,m十分大,我们用
L
u
c
a
s
Lucas
Lucas定理(若模数不为质数则使用
E
x
L
u
c
a
s
ExLucas
ExLucas定理扩展卢卡斯定理),公式
C
n
m
=
C
n
p
m
p
×
C
n
m
o
d
  
p
m
m
o
d
  
p
C_n^m=C_{\frac{n}{p}}^{\frac{m}{p}}\times C_{n\mod p}^{m\mod p}
Cnm=Cpnpm×Cnmodpmmodp。(具体证明与推导博主后期会另外总结_(¦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;
}
第五种. 对于只求 n n n一定的,而 m m m变化的,可以使用组合数的性质5递推即可,复杂度为 O ( n ) O(n) O(n),取模的话,预处理逆元即可,复杂度 O ( n + 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;