排列组合学习笔记
排列与组合
定义
- 组合数
从个不同元素中,任取个元素并成一组,叫做从个不同元素中取出个元素的一个组合;从个不同元素中取出个元素的所有组合的个数,叫做从个不同元素中取出个元素的组合数。
我们通常用大写字母表示,计算公式如下:
也可以写作或
- 排列数
排列,一般地,从个不同元素中取出个元素,按照一定的顺序排成一列,叫做从个元素中取出m个元素的一个排列。特别地,当时,这个排列被称作全排列。
我们通常用大写字母表示,计算公式如下:
也可写作或者
一些性质
- 组合数
性质1. 当或者时它的值为。特别的,当时它的值为。(这应该十分显然吧)
性质2. ,因为选个元素出来相当于你剩下个元素而选个元素出来,所以方案数是一样的。
性质3. ,这个就是组合数的一个递推式,它也是著名的杨辉三角,简单证明如下:
个元素里面选个元素有两种选法,第一种为在前个内选个,而不选第个,方案数为,或者在前个元素内,只选个,而第个选第个元素,方案数为,所以由加法原理得,个元素里面选个元素的方案数为。
性质4. ,也就是二项式展开的系数。
性质5. ,由定义式相除得。
- 排列数
性质1. ,显然。
性质2. ,这里这个公式将组合与排列联系起来了,我们可以这样理解,排列就是先选个元素出来的方案数,每种方案的个元素再进行排列方案数,所以总方案数根据乘法原理得。
- 补充
有重复元素的全排列
元素个数无限制:重复排列是一种特殊的排列。从个不同元素中可重复地选取个元素。按照一定的顺序排成一列,称作从个元素中取个元素的可重复排列。当且仅当所取的元素相同,且元素的排列顺序也相同,则两个排列相同。由分步记数原理易知,从n个元素中取m个元素的可重复排列的不同排列数为。
元素个数有限制:先有种元素,第种元素的个数为个,我们令。我们可以先对元素编号,使其变成无重复元素,那么总方案数为,根据乘法原理可知,我们令答案为,那么,所以公式为
可重复选择组合:设第个元素选个,转化为求方程的非负整数解的个数,我们转化一下,令,那么原方程就等于求的正整数解的个数,我们将其转化为个,然后使用插板法,将其插入个板子,每个间隔里的个数便是一个的值,那么原问题转化为在个位置上放个板子的方案数,且板子之间至少有一个间隔,那么答案就显然为,也就是。
程序上的实现
- 排列数
对于全排列,我们直接求阶乘,取模或者高精即可。对于重复元素全排列,我们使用快速幂即可在时间内解决。
对于普通的排列,我们直接套用公式,预处理以内的阶乘,然后和相除即可。若取模,求逆元相乘即可。
其余的情况求法类似。
- 组合数
第一种. 对于求一个组合数我们按照公式模拟,相除或者乘逆元即可,复杂度。
第二种. 对于求内的个组合数,我们使用杨辉三角的递推式即可,复杂度(其实跑不满,严格来说是)
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];
}
}
第三种. 对于求内的个组合数,我们预处理内的阶乘(若取模还要处理内阶乘的逆元),复杂度(加上逆元,其中为模数),每次求取时候用阶乘计算一下即可。
第四种. 对于取模的情况下,模数较小而十分大,我们用定理(若模数不为质数则使用定理扩展卢卡斯定理),公式。(具体证明与推导博主后期会另外总结_(¦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;
}
第五种. 对于只求一定的,而变化的,可以使用组合数的性质5递推即可,复杂度为,取模的话,预处理逆元即可,复杂度。
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;