排列组合
一.排列组合基础
1.加法,乘法原理: S= ∑ a i \sum{ai} ∑ai,S= ∏ a i \prod{ai} ∏ai
2.排列: 从n个不同元素中任选m个,组成一个排列,有
A
n
m
A_n^m
Anm种可能
3.组合: 从n个不同元素中任选m个,组成一个组合,有
C
n
m
C_n^m
Cnm种可能
4.二项式定理:(a+b)n= ∑ i = 0 n \sum_{i=0}^n ∑i=0n an-ibi
5.多重集的排列数:
- 若 S = ( n 1 ⋅ a 1 , n 2 ⋅ a 2 , … … , n k ⋅ a k ) S = (n_1\cdot a1,n_2\cdot a_2,……,n_k\cdot a_k) S=(n1⋅a1,n2⋅a2,……,nk⋅ak)
- 则 S S S 的全排列个数为 n ! ∏ i = 1 k n i ! \frac{n!}{ \prod_{i=1}^k n_i!} ∏i=1kni!n!
6.多重集组合数
- 若 S = ( n 1 ⋅ a 1 , n 2 ⋅ a 2 , … … , n k ⋅ a k ) S = (n_1\cdot a1,n_2\cdot a_2,……,n_k\cdot a_k) S=(n1⋅a1,n2⋅a2,……,nk⋅ak)
- 从中选择 r ( r < = n i ) r(r<=n_i) r(r<=ni) 个,一共有 C r + k − 1 k − 1 C_{r+k-1}^{k-1} Cr+k−1k−1 种方案。
- 方法:等价于使用插板法解决 x 1 + x 2 + … … x k = r x_1+x_2+……x_k=r x1+x2+……xk=r 的非负整数解的数目
7.不定方程整数解问题
- 问题1: n n n 个相同的苹果,分给 k k k 个人,求方案数。
- 解法1: 映射不定方程: x 1 + x 2 + … … + x k = n x_1+x_2+……+x_k=n x1+x2+……+xk=n。方案数为: C n − 1 k − 1 C_{n-1}^{k-1} Cn−1k−1
- 问题2: n n n 个相同的苹果,分给 k k k 个人,每人至少 1 个的方案数。
- 解法2: 映射不定方程: x i + x 2 + … … + x k = n x_i+x_2+……+x_k=n xi+x2+……+xk=n。令 y i = x i − 1 , y i > = 0 , y 1 + y 2 + … … + y k = n + k y_i=x_i-1,y_i>=0,y_1+y_2+……+y_k=n+k yi=xi−1,yi>=0,y1+y2+……+yk=n+k。则方案数为 C n + k − 1 k − 1 C_{n+k-1}^{k-1} Cn+k−1k−1
- 问题3: n n n 个相同的苹果,分给 k k k 个人,每个人至少 d i d_i di 个的方案数。
- 解法3: 映射不定方程: x i + x 2 + … … + x k = n x_i+x_2+……+x_k=n xi+x2+……+xk=n。令 y i = x i − d i , y i > = 0 , y 1 + y 2 + … … + y k = n + ∑ i = 1 k d i y_i=x_i-d_i,y_i>=0,y_1+y_2+……+y_k=n+\sum_{i=1}^kd_i yi=xi−di,yi>=0,y1+y2+……+yk=n+∑i=1kdi。则方案数为 C n + ∑ d − 1 k − 1 C_{n+\sum d -1}^{k-1} Cn+∑d−1k−1
8.错排列
- 设a为1 - n 的排列,ai != i 的排列的个数
- 方案数: D n = ( n − 1 ) ( D n − 1 + D n − 2 D_n=(n-1)(D_{n-1}+D_{n-2} Dn=(n−1)(Dn−1+Dn−2)
- 证明:
- 运用数学归纳法, D 0 = 1 , D 1 = 0 , D 2 = 1 D_0=1,D_1=0,D_2=1 D0=1,D1=0,D2=1
- 当 n > 2 n>2 n>2,数字 1 可放在位置 2-n 的任意一个,有 n-1 种可能
- 假设数字1放在位置2,考虑数字2放置的位置,
- 若数字2放在位置1,剩余 n-2 个数字错排,即 D n − 2 D_{n-2} Dn−2
- 若数字2不放在位置1,那么数字 3-n 均不能放在对应位置,而数字2也不能放在位置1,刚好就是这n-1个数字错排,即 D n − 1 D_{n-1} Dn−1
- 则: D n = ( n − 1 ) ( D n − 1 + D n − 2 ) D_n=(n-1)(D_{n-1}+D_{n-2}) Dn=(n−1)(Dn−1+Dn−2)
9.圆排列
- 从n个不同元素中选出m个元素,不分首尾的排成一个圆圈的排列。
- 其排列方案数: n ! m ( n − m ) ! \frac{n!}{m(n-m)!} m(n−m)!n!
二.排列组合练习题
例题1:插空法求不相邻排列
- 题目描述: 从n个位置中安放 k 个人,求不相邻的排列的方案数
- 方案数: C n + k − 1 k C_{n+k-1}^k Cn+k−1k
- 问题分析: 从 n 个位置中随便抽取 k 个后,还有 n-k 个位置,n-k 个位置存在 n-k+1 个空。从这 n-k+1 个空中任选 k 个空放入 k 个人,插入这 k 个数,则这 k 个人一定两两不相邻。
例题2:插空法与捆绑法的结合
- 题目描述: 有n个男生,m个女生,2名老师,他们准备排队。求任意两名女生不相邻且 2 名老师也不相邻的方案数 。
- 问题分析: 考虑容斥:ans = 任意两名女生不相邻的方案数 - 任意两名女生不相邻且 2 名老师相邻的方案数。前者:插空法,男生和老师一共 n+2 个,一共有 n+3 个空,方案数为 C n + 3 m × A n + 2 n + 2 × A m m C_{n+3}^m\times A_{n+2}^{n+2}\times A_m^m Cn+3m×An+2n+2×Amm 。后者:插空法+捆绑法,先将两名老师看成一共整体,男生和老师一共 n+1 个,一共 n+2 个空,方案数为 C n + 2 m × A n + 1 n + 1 × A m m × A 2 2 C_{n+2}^m\times A_{n+1}^{n+1}\times A_m^m\times A_2^2 Cn+2m×An+1n+1×Amm×A22 。最后答案相减即可。
例题3:插空法
- 题目描述: x 1 + x 2 + x 3 + x 4 + x 5 + x 6 = n , x > 0 x_1+x_2+x_3+x_4+x_5+x_6=n,x>0 x1+x2+x3+x4+x5+x6=n,x>0,则一共有多少 x 1 , x 2 … … x 6 x_1,x_2……x_6 x1,x2……x6 的取值。
- 问题分析: 等价于用五个隔板把 n 个位置隔开,一共有 n-1 个空,并且由于 x>0 ,不同隔板不能在同一个空里,则一共有 C n − 1 5 C_{n-1}^5 Cn−15。
例题4:组合dp
- 题目描述: 将n个不同的球放入m个不同的盒子,每个盒子至少放1个球,问有多少种分配方案?
- 问题分析: 设 f ( n , m ) f(n,m) f(n,m)为将 n 个不同的球放入 m 个盒子中的方案数。考虑第 n 个球放入的位置:若第 n 个球单独放在第 m 个盒子,剩下 n-1 个球放入前 m-1 个盒子中,方案数为 f ( n − 1 , m − 1 ) f(n-1,m-1) f(n−1,m−1);若剩下 n-1 个球放入前 m 个盒子,则第 n 个球可以随便放在 m 个盒子中的任意一个盒子里,方案数为 f ( n − 1 , m ) × m f(n-1,m)\times m f(n−1,m)×m 。由于m个盒子不同,所以还要乘上一个 m 个排列。
#include<bits/stdc++.h>
using namespace std;
long long f(long long n,long long m){
if(n<0||m<0||n<m)return 0;
if(n==1&&m==1)return 1;
return f(n-1,m-1)+f(n-1,m)*m;
}
long long fac(long long m){
long long ans=1;
for(int i=1;i<=m;i++)ans=ans*i;
return ans;
}
int main(){
long long n,m;
cin>>n>>m;
cout<<f(n,m)*fac(m);
return 0;
}
容斥原理
定理学习
- 奇加偶减
- 形式1: A 1 ∪ A 2 ∪ A 3 … … A n − 1 ∪ A n = ∑ ( k 个 元 素 的 交 ) × ( − 1 ) k A_1\cup A_2\cup A_3……A_{n-1}\cup A_n=\sum ( k 个元素的交)\times (-1)^k A1∪A2∪A3……An−1∪An=∑(k个元素的交)×(−1)k
- 形式2: A 1 ∩ A 2 ∩ A 3 … … A n − 1 ∩ A n = U − A ‾ 1 ∪ A ‾ 2 ∪ A ‾ 3 … … A ‾ n − 1 ∪ A ‾ n A_1\cap A_2\cap A_3……A_{n-1}\cap A_n=U-\overline A_1\cup \overline A_2\cup \overline A_3……\overline A_{n-1}\cup \overline A_n A1∩A2∩A3……An−1∩An=U−A1∪A2∪A3……An−1∪An
假的模板
void solve(){
long long ans=0,op=1;
for(int i=n;i>=1;i--){
ans+=f[i]*op;
op=-op;
}
}
例题练习
例题1: [ 1 , n ] [1,n] [1,n] 中有多少个数能被 x 或 y 整除
- 题目描述: 就是 [ 1 , n ] [1,n] [1,n] 中有多少个数能被 x 或者能被 y 整除
- 问题分析: 两个的并集 n x + n y \frac{n}{x}+\frac{n}{y} xn+yn。但由于最小公倍数在这两个集合中都被算了一次,需要减去这个交集部分。
- 答案: n x + n y − n l c m ( x , y ) \frac{n}{x}+\frac{n}{y}-\frac{n}{lcm(x,y)} xn+yn−lcm(x,y)n
例题2: [ 1 , n ] [1,n] [1,n] 中有多少个数能被 a i a_i ai 中的任意一个数
- 题目描述: 求 [ 1 , n ] [1,n] [1,n] 中有多少个数能被 m 个数 a i a_i ai 中的任意一个数整除的数的个数
- 问题分析: 求所有的交集:容斥板子。计算枚举的数的个数,奇加偶减即可。
- 写法: 用二进制 01 存储所有 a i a_i ai 的选取情况,去枚举所有的 a i a_i ai 选取情况,再根据容斥定理,选取个数为奇数的需要加上,选取个数为偶数的需要减去。时间复杂度 O ( 2 m ) O(2^m) O(2m)
long long GCD(long long a,long long b){
if(b==0)return a;;
else return gcd(b,a%b);
}
long long LCM(long long a,long long b){
return a/gcd(a,b)*b;
}
void slove(){
for(int i=1;i<(1<<m);i++){
long long op=0,lcm=1;
for(int j=0;j<m;j++){
if(i&(1<<j)){
op++;
lcm=LCM(lcm,a[j]);
}
}
if(op%2==1)ans+=n/lcm;
else ans-=n/lcm;
}
cout<<ans;
}
例题3:k 种颜色对一个一维数组染色,求相邻点颜色不同的方案数
- 题目描述: n 个连续的点在一维上,k 种颜色。求刚好用了 k 种颜色且相邻点颜色不同的方案数。
- 问题分析: 设 f [ i ] f[i] f[i] 为用了 < = i <=i <=i 种颜色且相邻的点颜色不同的方案数。对于这 n 个点,第一个点有 i 种可能,之后的每一个点只有 i-1 种可能,而这 i 种颜色从 k 种颜色中取,可能性有 C k i C_k^i Cki 种可能,则 f [ i ] = C k i × i × ( i − 1 ) n − 1 f[i]=C_k^i\times i\times (i-1)^{n-1} f[i]=Cki×i×(i−1)n−1。很明显:求交,容斥一下即可。
- 很明显,容斥一下: a n s = ∑ i = 1 k ( − 1 ) k − i × C k i × i × ( i − 1 ) n − 1 ans=\sum_{i=1}^k(-1)^{k-i}\times C_k^i\times i\times (i-1)^{n-1} ans=∑i=1k(−1)k−i×Cki×i×(i−1)n−1
例题4: [ 1 , n ] [1,n] [1,n] 中与 m 互质的数的个数
- 题目描述: 求 [ 1 , n ] [1,n] [1,n] 中与 m 互质的数的个数。
- 问题分析: 求与 m 互质,即求与 m 的每个质因子互质,(交集的形式)。容斥一下,求与任意一个质因子不互质的数的个数(这是并集的形式)。与质因子 x 不互质的个数: n − n x n-\frac{n}{x} n−xn 。枚举多个质因子时,与多个质因子均不互质的个数: n − n l c m n-\frac{n}{lcm} n−lcmn 。然后,奇加偶减即可。(注意: l c m lcm lcm 刚好就等于多个质因子的积)
- 写法: 同样用二进制 01 去表示是否选取了该因子。时间复杂度 ( O ( m 1 / 2 + 2 k × k ) ) (O(m^{1/2}+2^k\times k)) (O(m1/2+2k×k))
void init(){
for(int i=2;i*i<=m;i++){
if(m%i==0){
factor[num++]=i;
while(m%i==0)m=m/i;
}
}
if(m!=1)factor[num++]=m;
}
void slove(){
int ans=0;
for(int i=1;i<(1<<num);i++){
long long op=0,x=1;
for(int j=0;j<num;j++){
if(i&(1<<j)){
op++;
x*=factor[j];
}
}
if(op%2==1)ans+=n-n/x;
else ans-=n-n/x;
}
cout<<n-ans<<endl;
}
例题5:给定四种硬币面值及其数量,求有多少种方式凑成 s
- 题目描述: 给定四个硬币的面值 c i c_i ci 。T 组查询,每次查询给定四个硬币的数量 d i d_i di 和需要凑成的数字 s s s,求有多少种方式从中凑成 s 。 c i , d i , s ∈ [ 1 , 1 e 5 ] , T < = 1000 c_i,d_i,s\in[1,1e5],T<=1000 ci,di,s∈[1,1e5],T<=1000
- 问题分析: 看上去就是道多重背包模板题,但是( 1000 × 4 × 1 e 5 × l o g ( 1 e 5 ) ) 1000\times 4\times 1e5 \times log(1e5)) 1000×4×1e5×log(1e5)) 显然超时了。我们先把它当成完全背包看,并求出 f [ j ] f[j] f[j] 为 s = j s=j s=j 时的方案数 。此时由于硬币数量的限制,我们需要去除掉那些不合法的。根据差分的思想:如果我们求出了使用次数在 [ 0 , + ∞ ] [0,+\infty] [0,+∞] 的方案数和使用次数在 [ d i + 1 , + ∞ ] [d_i+1,+\infty] [di+1,+∞] 的方案数(即使用次数不合法的方案数),就可以通过做差求出使用次数在 [ 0 , d i ] [0,di] [0,di] 的方案数。考虑第 i 个物品不合法的情况,即第 i 个物品已经使用了 di+1 次,方案数为 f [ s − c i × ( d i + 1 ) ] f[s-ci\times (di+1)] f[s−ci×(di+1)]。由于有四种硬币,去重会重复的,因此需要容斥一下。
- 写法: 用二进制枚举,奇加偶减,最后减去即可。
#include<bits/stdc++.h>
using namespace std;
long long T,c[10],d[10],f[100010],s;
int main(){
for(int i=0;i<4;i++)cin>>c[i];
f[0]=1;
for(int i=0;i<4;i++){
for(int j=c[i];j<=100000;j++){
f[j]+=f[j-c[i]];
}
}
cin>>T;
while(T--){
for(int i=0;i<4;i++)cin>>d[i];
cin>>s;
long long t=0;
for(int i=1;i<(1<<4);i++){
long long op=0,x=s;
for(int j=0;j<4;j++){
if((1<<j)&i){
op++;
x-=c[j]*(d[j]+1);
}
}
if(x>=0){
if(op%2==1)t+=f[x];
else t-=f[x];
}
}
cout<<f[s]-t<<endl;
}
return 0;
}
例题6:m 个不同的数构成一个长度为 n 的符号码,求前 k 个数至少都使用一次的数符号码方案总数
- 题目描述: m 个不同的数构成一个长度为 n 的符号码,求前 k 个数至少都使用一次的数符号码方案总数 。
- 问题分析: 交的形式,先容斥一下变成并。即求前 k 个数字有没有用到数的方案总数。至少有一个数没有用到的方案总数为: C k 1 ( m − 1 ) n C_{k}^1(m-1)^n Ck1(m−1)n,至少有 i 个数都没有用到的方案总数为: C k i ( m − i ) n C_{k}^i (m-i)^n Cki(m−i)n 。再容斥一下即可。
- 公式答案:
- m n − ∑ i = 1 k C k i ( m − i ) n ( − 1 ) i − 1 = ∑ i = 0 k C k i ( m − i ) n ( − 1 ) i m^n-\sum_{i=1}^kC_{k}^i(m-i)^n(-1)^{i-1}=\sum_{i=0}^kC_{k}^i(m-i)^n(-1)^i mn−∑i=1kCki(m−i)n(−1)i−1=∑i=0kCki(m−i)n(−1)i
卡特兰数
简介
- 卡特兰数是一种著名数列
- 其通项公式为:f(n) = C 2 n n n + 1 \frac{C_{2n}^n}{n+1} n+1C2nn
- 其还有另一种变形:f(n) = C 2 n n C_{2n}^n C2nn - C 2 n n − 1 C_{2n}^{n-1} C2nn−1
基本模型
- 有一个长度为2n的01序列,其中1,0各n个,要求对于任意的前缀中,1的个数不小于0的个数,求满足该条件的序列的个数。
证明
我不会
拓展应用模型
以下模型均符合卡特兰数(我也不知道为什么)
- 2n个人排成一排进入剧院,入场费5元,有n个人有5元,有n个人有10元,售票处没有钱找零。求有多少种排队方法,使得不会出现没钱找零的情况。(人长得一样)
- 从(0,0)到(n,n),且不穿越(可触碰)直线 y = x 的道路总数。
- 圆上有2n个点,求将这些点成对连接起来使得所得到的n条线段不相交的方法数。
- 一个无穷大的栈的进栈序列为1,2,3……,n,有多少个不同的出栈序列。
- n个结点可构造成多少个不同的二叉树
- n个+1和n个-1构成2n项,求其任意前缀均大于等于0的序列的个数。
数论定理
Lucas定理
- 模型描述: 求 C n m m o d p C_{n}^m \bmod p Cnmmodp,当 n,m 过大,但 p 较小时
- L u c a s ( n , m , p ) = C n ( m o d p ) m ( m o d p ) × L u c a s ( n / p , m / p , p ) Lucas(n,m,p) = C_{n\pmod p}^{m\pmod p} \times Lucas(n/p,m/p,p) Lucas(n,m,p)=Cn(modp)m(modp)×Lucas(n/p,m/p,p)
- 实现方法:打表,Lucas递归
Code
#include<bits/stdc++.h>
using namespace std;
long long f[100010];
long long ksm(long long a,long long b,long long p){
long long ans=1;
while(b){
if(b&1)ans=ans*a%p;
a=a*a%p;
b=b/2;
}
return ans;
}
void init(long long p){
f[0]=1;
for(int i=1;i<=p;i++)f[i]=f[i-1]*i%p;
}
long long C(long long n,long long m,long long p){
if(n<m)return 0;
return (f[n]*ksm(f[n-m],p-2,p)%p*ksm(f[m],p-2,p)%p);
}
long long Lucas(long long n,long long m,long long p){
if(m==0)return 1;
return C(n%p,m%p,p)*Lucas(n/p,m/p,p)%p;
}
int main(){
//init();
int T;
long long n,m,p;
cin>>T;
while(T--){
cin>>n>>m>>p;
init(p);
cout<<Lucas(n+m,n,p)<<endl;
}
return n;
}
Prufer序列
- 学习目的: P r u f e r Prufer Prufer 序列可以方便的解决一类树相关的计数问题
- 定理描述: P r u f e r Prufer Prufer 序列与无根树一一对应。一棵 n 个结点的无根树,其对应的 P r u f e r Prufer Prufer 序列长度为 n − 2 n-2 n−2。
- 性质1: n n n 个结点的完全图的生成树一共有 n n − 2 n^{n-2} nn−2。解释: p r u f e r prufer prufer 每个位置均有 n n n 种可能。
- 性质2: 度数为 d i d_i di 的节点会在 p u r f e r purfer purfer 序列中出现 d i − 1 d_i-1 di−1 次。
- 性质3: 对于给定度数为 d 1 ∼ n d_{1\sim n} d1∼n 的无根树共有 ( n − 2 ) ! ∏ i = 1 n ( d i − 1 ) ! \frac{(n-2)!}{\prod_{i=1}^n(d_i-1)!} ∏i=1n(di−1)!(n−2)! 种情况。