组合数学初步
Double_Air
·
·
算法·理论
组合数学初步
数论初步链接
2026.8.23,已更新完毕!优化了一些格式内容。
本文将围绕数论进阶知识展开,讲解许多有关数论的知识,如果有问题,请指出,作者必将改进(前提你说的是对的)。
一些性质我不给出证明过程,还请谅解!
参考资料:OI-Wiki 数论板块;定理与算法为通用数学知识,证明、例题、行文逻辑为自主整理。
对于一些希腊字母符的读音,参照下图(上大写,下小写):
:::align{center}
::::
加法原理与乘法原理
加法原理
定义:完成一件事有 n 类不同的方案,第一类方案有 m_1 种方法,第 2 类方案有 m_2 种方法,……,第 n 类方案有 m_n 种方法,且这些方案之间互不重叠(即选择了一类就不能选另一类),那么完成这件事共有:
m_1+m_2+\cdots+m_n
种方法。通俗理解就是:“分类相加”。
乘法原理
定义:完成一件事有 n 个独立的步骤,第一步有 m_1 种方法,第 2 步有 m_2 种方法,……,第 n 步有 m_n 种方法,那么完成这件事共有:
m_1\times m_2\times\cdots\times m_n
种方法。通俗理解就是:“分步相乘”。
排列组合
排列
排列研究的是:从 n 个不同的元素中,按顺序选出 k 个元素,有多少种选法?
定义:从 n 个不同元素中取出 k 个(k\le n)排成一列,称为排列,记作 A_n^k 或 P(n,k),有计算公式:
A_n^k = n \times (n-1) \times \cdots \times (n-k+1) = \dfrac{n!}{(n-k)!}
怎么来的?根据乘法原理,第一个位置有 n 种选择,第二个位置有 n-1 种选择,……,第 k 个位置有 n-k+1 种选择,乘起来即可。
特别的,当 k=n 时,称为全排列,A_n^n=n!。
圆排列
定义:从 n 个不同元素中取出 k 个,排成一个圆环,称为圆排列。
与直线排列不同的是,圆排列没有“起点”,因此旋转后相同的排列被视为同一种。
有计算公式:圆排列数=\dfrac{A_n^k}{k}=\dfrac{n!}{k\cdot(n-k)!}
特别地,n 个元素的全圆排列数为 \dfrac{n!}{n}=(n-1)!。
组合
组合研究的是:从 n 个不同的元素中,无序地选出 k 个元素,有多少种选法?
定义:从 n 个不同元素中,取出 k 个(k\le n)元素并成一组,不考虑顺序,称为从 n 个元素中取 k 个元素的组合,记作 C_n^k 或 \dbinom{n}{k}(注意是反过来的)。
有计算公式:
C_n^k=\dfrac{A_n^k}{k!}=\dfrac{n!}{k!(n-k)!}
怎么来的?由于组合数 C_n^k 表示无序地选出 k 个元素。而排列数 A_n^k 是在组合的基础上,再对选出的 k 个元素进行全排列。
选出的 k 个元素的全排列数为:k!,所以我们得到如下组合与排列的关系:
A_n^k=C_n^k\times k!
将这个关系式简单移项化简就可以得到结论了。
组合数的一些性质
- 对称性\binom{n}{k}=\binom{n}{n-k}
- 帕斯卡恒等式\binom{n}{k}=\binom{n-1}{k-1}+\binom{n-1}{k}
- 求和公式(n 个元素的所有子集个数)\sum^n_{i=0}\binom{n}{i}=2^n
多重集组合(隔板法)
定义:从 n 类元素中(每类元素有无限多个,同类元素完全相同)取出 k 个,叫做多重集组合数,计算公式:
\binom{n+k-1}{k}=\Large C_{n+k-1}^k
这就相当于将 k 个相同的球放入 n 个不同的盒子中,允许空盒。等同于在 k+n-1 个位置中选 n-1 个位置放隔板,即
\binom{n+k-1}{n-1}=\binom{n+k-1}{k}
多重集和多重集的全排列
多重集
由于普通集合(set)中的元素不能重复,所以就有了多重集合(multiset)使得其中的元素可以重复。
多重集的全排列
若多重集中有 n 个元素,其中第 i 种元素出现了 k_i 次(\sum k_i=n),则全排列数为:
\dfrac{n!}{k_1!\cdot k_2!\cdots k_m!}
推导过程十分简单,先当作所有元素都不同,有 n! 种,再除以同类元素的内部排列数 k_i!。
解析:这个单词里 $\texttt{e}$ 出现了一次,$\texttt{k}$ 出现了两次,$\texttt{i}$ 出现了三次,$\texttt{t}$ 出现了一次,共有七个字符,所以有:
$$\dfrac{7!}{1!\cdot2!\cdot3!\cdot1!}=\dfrac{5040}{12}=420$$
到这里可以发现,当只有两类元素时,全排列数就跟我们的组合数一样了,所以,我们有:**组合数本质上是两类元素的多重集全排列数**。
### 求组合数的代码实现
#### 预处理阶乘与逆元
当模数 $p$ 为质数且 $n\le 10^6$ 时,可以使用预处理阶乘与逆元(费马小定理),代码如下:
```cpp
#include<bits/stdc++.h>
#define int long long
#define rep(z,x,y) for(int z=x;z<=y;z++)
#define drep(z,x,y) for(int z=x;z>=y;z--)
using namespace std;
const int MAXN=1000005;
const int MOD=1e9+7;
int fac[MAXN],ifc[MAXN];
int fpw(int a,int b){
int r=1;
while(b){
if(b&1) r=r*a%MOD;
a=a*a%MOD;
b>>=1;
}
return r;
}
void init(int n){
fac[0]=1;
rep(i,1,n){
fac[i]=fac[i-1]*i%MOD;
}
ifc[n]=fpw(fac[n],MOD-2);
drep(i,n-1,0){
ifc[i]=ifc[i+1]*(i+1)%MOD;
}
}
int C(int n,int k){
if(k<0||k>n)
return 0;
return fac[n]*ifc[k]%MOD*ifc[n-k]%MOD;
}
```
#### 递推法(杨辉三角)
当且仅当 $n\le 2000$ 时,可以递推求解,代码如下:
```cpp
#include<bits/stdc++.h>
#define int long long
#define rep(z,x,y) for(int z=x;z<=y;z++)
#define drep(z,x,y) for(int z=x;z>=y;z--)
using namespace std;
const int MAXN=2005;
int C[MAXN][MAXN];
void initC(int n){
rep(i,0,n){
C[i][0]=C[i][i]=1;
rep(j,1,i-1){
C[i][j]=(C[i-1][j-1]+C[i-1][j])%MOD;
}
}
}
```
#### 多重集全排列
我们知道**组合数本质上是两类元素的多重集全排列数**,所以可以写得如下代码:
```cpp
#include<bits/stdc++.h>
#define int long long
#define rep(z,x,y) for(int z=x;z<=y;z++)
#define drep(z,x,y) for(int z=x;z>=y;z--)
using namespace std;
int fac(int n){
int r=1;
rep(i,2,n){
r*=i;
}
return r;
}
int msp(vector<int>&cnt,int n){
int ans=fac(n);
for(int c:cnt){
ans/=fac(c);
}
return ans;
}
```
给一道组合数例题:[[GESP202312 八级] 奖品分配](https://www.luogu.com.cn/problem/P10112)
## 等价关系与等价类
### 等价关系
定义:在一个集合 $S$ 上,如果有一个关系 $\sim$ (等价于),它对任意元素 $a,b,c\in S$ 都满足以下三个性质,则称 $\sim$ 是 $S$ 上的一个等价关系:
- 自反性:$a\sim a$(每个元素和自己等价)
- 对称性:若 $a\sim b$,则 $b\sim a$(对称)
- 传递性:若 $a\sim b$ 且 $b\sim c$,则 $a\sim c$(传递)
最直观的例子就是**模运算同余**。
### 等价类
定义:把集合里的元素按关系分组。每一组叫做一个等价类。
元素 $a$ 所在的等价类,记作 $[a]$,定义为:
$$[a]=\{x\in S\mid x\sim a\}$$
即所有与 $a$ 等价的元素组成的集合。
以**模 $3$ 同余**为例,整数集 $\mathbb Z$ 被分成了 $3$ 个等价类:
- $[0]=\{\dots,-6,-3,0,3,6,\dots\}$(余数为 $0$ 的数)
- $[1]=\{\dots,-5,-2,1,4,7,\dots\}$(余数为 $1$ 的数)
- $[2]=\{\dots,-4,-1,2,5,8,\dots\}$(余数为 $2$ 的数)
每一个等价类里的数,除以 $3$ 的余数都相同。这些类互不相交,且并起来就是整个 $\mathbb Z$。
等价关系就是“去重”或“去顺序”的数学依据,而我们的**组合数,圆排列数都等于等价类的个数**。
## 二项式定理
在数学中,我们经常碰到这样一个问题:
$$(a+b)^n$$
想必大家在遇到 $n$ 比较小的时候会暴力拆解吧,但是,有一招很简单的方法,几乎秒出答案。
当展开 $(a+b)^n$ 时,每一项都是从 $n$ 个括号中选若干个取 $b$,其余选 $a$ 的结果。
- 假设 $k$ 个括号取 $b$,剩下 $n-k$ 个取 $a$。
- 那么它的贡献为 $a^{n-k}b^k$。
- 总共有 $\large C_n^k$ 种选法。
所以,我们得出结论:
$$(a+b)^n=\sum^n_{k=0}\binom{n}{k}a^{n-k}b^k$$
### 数学归纳法证明
验证 $n=1$ 的时候原式成立:
$$(a+b)^1=\binom{0}{1}a+\binom{1}{0}b=a+b$$
假设 $n$ 时原式也成立,证明 $n+1$ 时也成立:
$$(a+b)^{n+1}=(a+b)(a+b)^n=(a+b)\sum^n_{k=0}\binom{n}{k}a^{n-k}b^k$$
展开得到:
$$=\sum^n_{k=0}\binom{n}{k}a^{n-k+1}b^k+\sum^n_{k=0}\binom{n}{k}a^{n-k}b^{k+1}$$
我们令 $j=k+1$:
$$=\sum^n_{k=0}\binom{n}{k}a^{n-k+1}b^k+\sum^{n+1}_{j=1}\binom{n}{j-1}a^{n-j+1}b^{j}$$
合并同类项,利用帕斯卡恒等式 $\Large\binom{n}{k}=\binom{n-1}{k-1}+\binom{n-1}{k}$ 得到:
$$=\sum^{n+1}_{k=0}\binom{n+1}{k}a^{n-k+1}b^k$$
### 与杨辉三角的关系
我们枚举 $n=0,1,2,3,4,5$ 的情况观察一下:
$$(a+b)^0=1$$
$$(a+b)^1=a+b$$
$$(a+b)^2=a^2+2ab+b^2$$
$$(a+b)^3=a^3+3a^2b+3ab^2+b^3$$
$$(a+b)^4=a^4+4a^3b+6a^2b^2+4ab^3+b^4$$
$$(a+b)^5=a^5+5a^4b+10a^3b^2+10a^2b^3+5ab^4+b^5$$
我们将上述系数排列出来,可以得到**杨辉三角**:
```cpp
n=0: 1
n=1: 1 1
n=2: 1 2 1
n=3: 1 3 3 1
n=4: 1 4 6 4 1
n=5: 1 5 10 10 5 1
```
且第 $n$ 行的第 $k$ 个数就是 $\Large\binom{n}{k}$。
## 一些推论
### 推论1:令 $a=1,b=1
2^n=\sum^n_{k=0}\binom{n}{k}
含义:n 个元素的所有子集个数为 2^n。这也体现了组合数学与二进制的关系。
推论2:令 a=1,b=-1
0=\sum^n_{k=0}(-1)^k\binom{n}{k}\iff\binom{n}{0}+\binom{n}{2}+\binom{n}{4}+\dots=\binom{n}{1}+\binom{n}{3}+\binom{n}{5}+\dots=2^{n-1}
含义:偶数大小子集和奇数大小子集各占一半。
推论3:令 b=1
(1+a)^n=\sum^n_{k=0}\binom{n}{k}a^k
这就是生成函数的雏形。
推论4:求导
对 (1+a)^n=\sum^n_{k=0}\binom{n}{k}a^k 求导:
n(1+a)^{n-1}=\sum^n_{k=1}k\binom{n}{k}a^{k-1}
我们令 a=1:
n\cdot2^{n-1}=\sum^n_{k=1}k\binom{n}{k}
推论5:曲棍球棒恒等式
标准的曲棍球棒恒等式是:
\sum^k_{i=0}\binom{r+i}{i}=\binom{r+k+1}{k}
如果我们令 r=n-1,就会得到:
\sum^k_{i=0}\binom{n-1+i}{i}=\binom{n+k}{k}
推论6:范德蒙德卷积
\binom{a+b}{n}=\sum^n_{i=0}\binom{a}{i}\binom{b}{n-i}\iff\binom{a+b}{n}=\sum^n_{i=0}\binom{a}{n-i}\binom{b}{i}
我们令 a=b=n 可以得到:
\binom{2n}{n}=\sum^n_{i=0}\binom{n}{i}^2
再令 b=-b 呢?
\binom{a-b}{n}=\sum^n_{i=0}\binom{a}{i}\binom{-b}{n-i}
上面的公式就是广义的范德蒙德卷积。
负组合数
当上标为负数时,组合数的定义为:
\binom{-n}{m}=\dfrac{(-n)(-n-1)(-n-2)\cdots(-n-m+1)}{m!}
广义二项式定理
当指数 n 不是非负整数时,二项式定理可以推广到实数或复数指数:
(1+x)^{\alpha}=\sum^{\infty}_{k=0}\binom{\alpha}{k}x^k
\text{e.g.}\quad\alpha=-1
(1+x)^{-1}=1-x+x^2-x^3+\cdots=\sum^{\infty}_{k=0}(-1)^k x^k
三项展开(多项式定理)
(a+b+c)^n=\sum_{i+j+k=n}\dfrac{n!}{i!j!k!}a^ib^jc^k
证明方式跟二项式定理的证明几乎一模一样,这里不再解释。
对于任意 m 项:
(x_1+x_2+x_3+\cdots+x_m)^n=\sum_{k_1+k_2+k_3+\cdots+k_m=n}\dfrac{n!}{k_1!k_2!k_3!\cdots k_m!}x_1^{k_1}x_2^{k_2}x_3^{k_3}\cdots x_m^{k_m}
这是二项式定理的推广,系数为多项分布系数(多重集全排列数)。
二项式反演
二项式反演是组合数学中最重要的反演公式之一。它描述了两种组合数求和之间的互逆关系:
如果:
f(n)=\sum^n_{i=0}\binom{n}{i}g(i)
那么:
g(n)=\sum^n_{i=0}(-1)^{n-i}\binom{n}{i}f(i)
这两个公式互为反演,即两个可以相互推导出来。
简单证明一下:代入即可,核心是使用恒等式 \large\binom{n}{i}\binom{i}{j} = \binom{n}{j}\binom{n-j}{i-j} 和二项式定理。
错位排列
定义:n 个元素的一个排列,使得没有任何一个元素处于它原来的位置。记作 D_n 或 !n
列举一下前几项:
D_1=0,D_2=1,D_3=2,D_4=9,D_5=44,D_6=265
从而得到递推公式:
D_n=(n-1)(D_{n-1}+D_{n-2})
怎么来的?考虑第 n 个元素放在位置 k(k\ne n),有 n-1 种选择。而位置 k 上的元素有两种去向:
- 放到位置 n,剩下 n-2 个元素错排:D_{n-2}。
- 不放到位置 n,剩下 n-1 个元素错排:D_{n-1}。
近似值
如果 n 超级大时:
D_n\approx\dfrac{n!}{e}
因为 \sum^{\infty}_{j=0}\dfrac{(-1)^j}{j!}=e^{-1},所以错位排列数约等于全排列数的 \dfrac{1}{e}\approx36.8\%。
代码实现
递推法
#include<bits/stdc++.h>
#define int long long
#define rep(z,x,y) for(int z=x;z<=y;z++)
#define drep(z,x,y) for(int z=x;z>=y;z--)
using namespace std;
const int maxn=25;
int f[maxn],n;
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr),cout.tie(nullptr);
cin>>n;
f[1]=0;
f[2]=1;
f[3]=2;
if(n==1||n==2||n==3){
cout<<f[n]<<'\n';
return 0;
}
rep(i,4,n){
f[i]=(i-1)*(f[i-1]+f[i-2]);
}
cout<<f[n]<<'\n';
return 0;
}
抽屉原理
抽屉原理(Pigeonhole Principle),也叫鸽巢原理,是组合数学中最简单、最直观,却也最深刻的原理之一。
它的表述极其简单,但应用起来却变化万千,能解决许多看似困难的问题。有人说:“抽屉原理是数学中少数几个既显然又深刻的定理之一。”
基本形式
- 朴素版:把 n+1 个物品放入 n 个抽屉,至少有一个抽屉有至少 2 个物品。
- 一般版:把 m 个物品放入 n 个抽屉,至少有一个抽屉有至少 \lceil\dfrac{m}{n}\rceil 个物品。
证明也比较简单,使用反证法。若每个抽屉最多放 \lceil\dfrac{m}{n}\rceil-1 个,则总数最多为:
n\times(\lceil\dfrac{m}{n}\rceil-1)<m
这就矛盾了,所以得证。
一些应用
-
生日问题:一年有 12 个月,当学生数为 13 时,必有两人生日在同一个月。
-
握手问题:在 n 个人的聚会中,必有两个人的握手次数相同。
- 这个证明也挺简单,每个人的握手次数在 0\sim n-1,共 n 种可能。但不可能同时有人握了 0 次和 n-1 次,所以实际只有 n-1 种可能的握手次数,而有 n 个人,必有两个相同。
-
连续整数和问题:任意 n 个整数中,必存在若干个连续整数,其和能被 n 整除。
容斥原理
容斥原理(Inclusion-Exclusion Principle)是组合计数中最重要的原理之一。它的核心思想是:先不考虑限制条件,把所有的都算上,然后把多算的减掉,再把减多了的加回来……
这个过程形象地称为 “多退少补” 。
基本形式
对于两个有限集合 A 和 B,有:
\lvert A\cup B\rvert=\lvert A\rvert+\lvert B\rvert-\lvert A\cap B\rvert
在小学的时候,我们学过维恩图,他就是两个圆重叠,\lvert A\rvert+\lvert B\rvert 将重叠部分 \lvert A\cap B\rvert 算了两次,所以需要减去一次。
$$\lvert A_2\cup A_3\rvert=\lvert A_2\rvert+\lvert A_3\rvert-\lvert A_2\cap A_3\rvert=50+33-16=67$$
扩展一下呢?对于三个有限集合 $A,B,C$,有:
$$\lvert A\cup B\cup C\rvert=\lvert A\rvert+\lvert B\rvert+\lvert C\rvert-\lvert A\cap B\rvert-\lvert B\cap C\rvert-\lvert A\cap C\rvert+\lvert A\cap B\cap C\rvert$$
三个圆重叠,先加三个单独区域(多加了两次两两重叠),减掉三个两两重叠(又多减了一次三重重叠),再加回三重重叠。
如果我们扩展到 $n$ 个呢?对于 $n$ 个有限集合 $A_1,A_2,\dots A_n$,有:
$$
\left|\bigcup_{i=1}^{n} A_i\right|
= \sum_{i=1}^{n}|A_i|
- \sum_{1\le i<j\le n}|A_i\cap A_j|
+ \sum_{1\le i<j<k\le n}|A_i\cap A_j\cap A_k|
-\dots + (-1)^{n-1}\left|A_1\cap A_2\cap \dots \cap A_n\right|
$$
用求和符号简写:
$$
\left|\bigcup_{i=1}^{n} A_i\right|
= \sum_{\varnothing\neq S\subseteq \{1,2,\dots,n\}}
(-1)^{|S|+1}
\left|\bigcap_{i\in S} A_i\right|
$$
### 证明
方法:数学归纳法
第 $1$ 步:$n=1$ 时显然成立:$|A_1| = |A_1|$。
第 $2$ 步:$n=2$ 时成立,作为归纳基础。
第 $3$ 步:假设 $n-1$ 时成立,证明 $n$ 时也成立。
令 $B = A_1 \cup A_2 \cup \dots \cup A_{n-1}$,则:
$$\left|\bigcup_{i=1}^{n} A_i\right|= |B \cup A_n|= |B| + |A_n| -|B\cap A_n|$$
而:
$$
B \cap A_n = (A_1 \cap A_n) \cup (A_2 \cap A_n) \cup \dots \cup (A_{n-1} \cap A_n)$$
对 $n-1$ 个集合 $A_1\cap A_n,\,A_2\cap A_n,\dots,A_{n-1}\cap A_n$,应用归纳假设:
$$|B \cap A_n|=\sum_{\varnothing\neq S\subseteq \{1,\dots,n-1\}}(-1)^{|S|+1}\left|\bigcap_{i\in S}(A_i \cap A_n)\right|$$
代入整理即可得到 $n$ 时的公式。
### 应用(欧拉函数)
设 $n = p_1^{\alpha_1} p_2^{\alpha_2}\cdots p_k^{\alpha_k}$,求 $\varphi(n)$。
$\varphi(n)$ 表示 $1$ 到 $n$ 中与 $n$ 互质的数的个数。
设 $A_i$ 表示 $1$ 到 $n$ 中能被 $p_i$ 整除的数的集合。
$$|A_i|=\frac{n}{p_i},\quad |A_i\cap A_j|=\frac{n}{p_ip_j},\ \dots$$
需要排除所有能被至少一个 $p_i$ 整除的数:
$$\begin{aligned}\varphi(n) &= n-\sum_i \frac{n}{p_i}+\sum_{i<j}\frac{n}{p_ip_j}-\cdots \\&= n\left(1-\sum_i \frac{1}{p_i}+\sum_{i<j}\frac{1}{p_ip_j}-\cdots\right) \\&= n\prod_{i=1}^{k}\left(1-\frac{1}{p_i}\right)\end{aligned}$$
而这就是欧拉函数的通项公式,与数论初步中的内容完美呼应。
### 一个问题的代码实现
给定一个 $n$,让你求 $1\sim n$ 中与 $m$ 互质的个数。
思路就是对 $m$ 进行质因数分解,然后用容斥原理扣除能被某个质因子整除的数。代码如下:
```cpp
#include<bits/stdc++.h>
#define int long long
#define rep(z,x,y) for(int z=x;z<=y;z++)
#define drep(z,x,y) for(int z=x;z>=y;z--)
using namespace std;
int cpc(int n,int m){
vector<int>pr;
int tmp=m;
for(int i=2;i*i<=tmp;i++){
if(tmp%i==0){
pr.push_back(i);
while(tmp%i==0)
tmp/=i;
}
}
if(tmp>1)
pr.push_back(tmp);
int k=pr.size();
int ans=n;
for(int ms=1;ms<(1<<k);ms++){
int bt=__builtin_popcount(ms);
int prod=1;
for(int i=0;i<k;i++){
if(ms&(1<<i))
prod*=pr[i];
}
if(bt&1)
ans-=n/prod;
else
ans+=n/prod;
}
return ans;
}
```
### 对偶形式
对于补集,有对偶形式:
$$\left|\bigcap_{i=1}^{n} A_i\right|=\lvert U\rvert-\left|\bigcup_{i=1}^n\overline{A_i}\right|$$
即:同时满足所有条件的个数 = 总数 - 至少不满足一个条件的个数。
## 卡特兰数
卡特兰数(Catalan Number)是组合数学中出现频率最高的数列之一。它计数的问题多到令人惊讶——从括号匹配到二叉树,从进出栈到多边形剖分,看似完全不相干的问题,答案竟然都是同一个数列。
列举一下前几项:
$$1,1,2,5,14,42,132,429,1430,4862$$
### 定义
卡特兰数 $C_n$(注意不是组合数),有多种等价的定义方式:
**递推定义:**
$$C_0=1,\quad C_{n+1}=\sum^n_{i=0}C_iC_{n-i}$$
**通项公式:**
$$C_n=\dfrac{1}{n+1}\binom{2n}{n}=\binom{2n}{n}-\binom{2n}{n+1}$$
#### 对于通项公式的推导:
##### 生成函数法
设卡特兰数的生成函数为 $F(x)=\sum^{\infty}_{n=0}C_nx^n$。
由递推式 $C_{n+1}=\sum^n_{i=0}C_iC_{n-i}$ 得到:
$$F(x)-1=xF(x)^2$$
来解这个二次方程:
$$F(x)=\dfrac{1-\sqrt{1-4x}}{2x}$$
展开 $\sqrt{1-4x}$,比较系数可以得到:
$$C_n=\dfrac{1}{n+1}\binom{2n}{n}$$
### 一些运用
- $n$ 对括号的合法匹配。
- $n$ 个元素依次进栈,有多少种不同的出栈序列。
- 具有 $n$ 个节点的不同形态的二叉树有多少种。
- 一个凸 $n+2$ 边形,可以划分成多少个三角形。
### 代码实现
#### 通项公式法
当 $n$ 在 $10^6$ 的级别时,且模数 $p$ 为质数时:
```cpp
#include<bits/stdc++.h>
#define int long long
#define rep(z,x,y) for(int z=x;z<=y;z++)
#define drep(z,x,y) for(int z=x;z>=y;z--)
using namespace std;
const int MAXN=2000005;
const int MOD=1e9+7;
int fac[MAXN],ifc[MAXN];
int fpw(int a,int b){
int r=1;
while(b){
if(b&1)
r=r*a%MOD;
a=a*a%MOD;
b>>=1;
}
return r;
}
void init(int n){
fac[0]=1;
rep(i,1,n){
fac[i]=fac[i-1]*i%MOD;
}
ifc[n]=fpw(fac[n],MOD-2);
drep(i,n-1,0){
ifc[i]=ifc[i+1]*(i+1)%MOD;
}
}
int C(int n,int k){
if(k<0||k>n)
return 0;
return fac[n]*ifc[k]%MOD*ifc[n-k]%MOD;
}
int cat(int n){
return C(2*n,n)*fpw(n+1,MOD-2)%MOD;
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr),cout.tie(nullptr);
init(2000000);
cout<<cat(3)<<'\n';
cout<<cat(4)<<'\n';
cout<<cat(5)<<'\n';
return 0;
}
```
#### 递推法
当不需要取模,或 $n$ 较小时:
```cpp
int cat(int n){
vector<int>c(n+1);
c[0]=1;
rep(i,1,n){
c[i]=c[i-1]*2*(2*i-1)/(i+1);
}
return c[n];
}
```
递推的公式推导也比较简单:
$$C_n = \dfrac{1}{n+1}\binom{2n}{n},\quad C_{n-1} = \dfrac{1}{n}\binom{2n-2}{n-1}$$
$$\dfrac{C_n}{C_{n-1}}=\dfrac{\dfrac{1}{n+1}\binom{2n}{n}}{\dfrac{1}{n}\binom{2n-2}{n-1}}=\dfrac{n}{n+1}\cdot\dfrac{(2n)(2n-1)}{n^2}=\dfrac{2(2n-1)}{n+1}$$
所以:
$$C_n=\dfrac{2(2n-1)}{n+1}C_{n-1}$$
## 斯特林数
斯特林数(Stirling Number)是组合数学中研究划分结构的核心工具,分为两大类:
| 类型 | 研究对象 | 记法 |
|:-:|:-:|:-:|
| 第一类斯特林数 | 排列成轮换 | $s(n,k)$ 或 $c(n,k)$ |
| 第二类斯特林数 | 划分成子集 | $S(n,k)$ 或 $\large\left\{n \atop k\right\}$ |
### 第二类斯特林数
定义:$S(n,k)$ 表示将 $n$ 个不同元素划分成 $k$ 个非空子集的方案数。
#### 递推公式
$$S(n,k)=S(n-1,k-1)+k\cdot S(n-1,k)$$
对于初始值 $S(0,0)=1,S(n,0)=0,S(n,n)=1$。
推导:考虑第 $n$ 个元素:
1. 自成一组:剩下 $n-1$ 个元素分为 $k-1$ 组,方案数为 $S(n-1,k-1)$。
2. 加入已有组:先给剩下 $n-1$ 个元素分成 $k$ 组,共有 $S(n-1,k)$ 种分法。第 $n$ 个元素可以加入其中任意一组,有 $k$ 中选择。
两种情况相加即得递推式。
#### 通项公式
$$S(n,k)=\dfrac{1}{k!}\sum^k_{i=0}(-1)^{k-i}\binom{k}{i}i^n$$
推导思路:用容斥原理。先给每个元素分配一个组编号($k^n$种),减去有空组的方案,最后除以 $k!$ 消去组编号的顺序。
#### 前几项:
$$\begin{array}{c|ccccccc}n\backslash k & 0 & 1 & 2 & 3 & 4 & 5 & 6 \\\hline0 & 1 \\1 & 0 & 1 \\2 & 0 & 1 & 1 \\3 & 0 & 1 & 3 & 1 \\4 & 0 & 1 & 7 & 6 & 1 \\5 & 0 & 1 & 15 & 25 & 10 & 1 \\6 & 0 & 1 & 31 & 90 & 65 & 15 & 1\end{array}$$
#### 代码实现
##### 递推法
```cpp
#include<bits/stdc++.h>
#define int long long
#define rep(z,x,y) for(int z=x;z<=y;z++)
#define drep(z,x,y) for(int z=x;z>=y;z--)
using namespace std;
const int MOD=1e9+7;
int s2(int n,int k){
if(k<0||k>n)
return 0;
vector<vector<int>>S(n+1,vector<int>(k+1,0));
S[0][0]=1;
rep(i,1,n){
rep(j,1,min(i,k)){
S[i][j]=(S[i-1][j-1]+j*S[i-1][j])%MOD;
}
}
return S[n][k];
}
```
##### 通项公式法
```cpp
int s2f(int n,int k){
int ans=0;
rep(i,0,k){
int tm=C(k,i)*fpw(i,n)%MOD;
if((k-i)&1)
ans=(ans-tm+MOD)%MOD;
else
ans=(ans+tm)%MOD;
}
return ans*ifc[k]%MOD;
}
```
### 第一类斯特林数
定义:$s(n,k)$ 表示将 $n$ 个不同元素划分成 $k$ 个非空轮换的方案数。
什么是轮换?轮换(cycle)是一种环形排列。例如,$(1\ 2\ 3)$ 表示 $1 \to 2 \to 3 \to 1$ 的循环。
轮换 $(1\ 2\ 3)$ 和 $(2\ 3\ 1)$ 表示同一个轮换(只是起点不同),但 $(1\ 3\ 2)$ 是另一个轮换。
$\text{e.g.}$ $3$ 个元素排列成 $2$ 个轮换:
- $(1)\circ(2\ 3)$:$1$ 不动,$2$ 和 $3$ 互换
- $(2)\circ(1\ 3)
共 3 种,所以 s(3,2)=3。
两种版本
第一类斯特林数有两种版本:
| 版本 |
记法 |
递推公式 |
关系 |
| 无符号 |
c(n,k) |
c(n,k)=c(n-1,k-1)+(n-1)c(n-1,k) |
c(n,k)\ge 0 |
| 有符号 |
s(n,k) |
s(n,k)=s(n-1,k-1)-(n-1)s(n-1,k) |
s(n,k)=(-1)^{n-k}c(n,k) |
对于初值:c(0,0)=s(0,0)=1,\ c(n,0)=s(n,0)=0\ (n>0)。
关系:
s(n,k)=(-1)^{n-k}c(n,k)
递推公式推导
考虑第 n 个元素:
- 自成轮换:剩下 n-1 个元素排成 k-1 个轮换,方案数为 c(n-1,k-1)。
- 插入已有轮换:先给剩下 n-1 个元素排成 k 个轮换,共有 c(n-1,k) 种。第 n 个元素可以插入任意一个轮换的任意一个元素后面,有 n-1 种选择。
所以:
c(n,k)=c(n-1,k-1)+(n-1)\cdot c(n-1,k)
有符号版本:
s(n,k)=s(n-1,k-1)-(n-1)\cdot s(n-1,k)
前几项
\begin{array}{c|ccccccc}n\backslash k & 0 & 1 & 2 & 3 & 4 & 5 & 6 \\\hline0 & 1 \\1 & 0 & 1 \\2 & 0 & 1 & 1 \\3 & 0 & 2 & 3 & 1 \\4 & 0 & 6 & 11 & 6 & 1 \\5 & 0 & 24 & 50 & 35 & 10 & 1 \\6 & 0 & 120 & 274 & 225 & 85 & 15 & 1\end{array}
代码实现
int s1(int n,int k){
if(k<0||k>n)
return 0;
vector<vector<int>>s(n+1,vector<int>(k+1,0));
s[0][0]=1;
rep(i,1,n){
rep(j,1,min(i,k)){
s[i][j]=(s[i-1][j-1]+(i-1)*s[i-1][j])%MOD;
}
}
return s[n][k];
}
两类斯特林数的联系
若将 x^n 和 x^{\underline{n}} 用两类斯特林数联系:
x^n=\sum_{k=0}^{n} S(n,k)\cdot x^{\underline{k}}
x^{\underline{n}}=\sum_{k=0}^{n} s(n,k)\cdot x^{k}
其中 x^{\underline{n}}=x(x-1)(x-2)\cdots(x-n+1) 是下降阶乘。
这意味着两类斯特林数的矩阵互为逆矩阵。
贝尔数
定义:贝尔数 B_n 表示将 n 个不同元素划分成若干个非空子集的方案总数。
B_n=\sum_{k=0}^{n} S(n,k)
其中 S(n,k) 是第二类斯特林数。
列举一下前几项:
B_0=1,\,B_1=1,\,B_2=2,\,B_3=5,\,B_4=15,\,B_5=52,\,B_6=203,\dots
递推公式
B_{n+1}=\sum_{k=0}^{n}\binom{n}{k}B_k
推导:先选定包含元素 n+1 的子集,从剩下 n 个元素中选 k 个与它放在一起,剩下 n-k 个元素任意划分。
代码实现
int bel(int n){
vector<int>b(n+1);
b[0]=1;
rep(i,0,n-1){
b[i+1]=0;
rep(k,0,i){
b[i+1]=(b[i+1]+C(i,k)*b[k])%MOD;
}
}
return b[n];
}
给一道例题吧,比较综合:十二重计数法【弱化版】。
本题就是考察十二种盒子与球的计数方法,本人的参考代码:
::::info[十二重计数法【弱化版】代码]
include<bits/stdc++.h>
define int long long
define rep(z,x,y) for(int (z)=(x);(z)<=(y);(z)++)
define drep(z,x,y) for(int (z)=(x);(z)>=(y);(z)--)
using namespace std;
const int MOD=998244353;
const int MAXN=2005;
int f[MAXN],niyuan[MAXN],s[MAXN][MAXN],b[MAXN][MAXN];
int C(int n,int k){
if(k<0||k>n) return 0;
return f[n]niyuan[k]%MODniyuan[n-k]%MOD;
}
int qpow(int a,int b){
int r=1;
while(b){
if(b&1) r=ra%MOD;
a=aa%MOD;
b>>=1;
}
return r;
}
signed main(){
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
int n,m;
cin>>n>>m;
f[0]=1;
rep(i,1,MAXN-1){
f[i]=if[i-1]%MOD;
}
niyuan[MAXN-1]=qpow(f[MAXN-1],MOD-2);
drep(i,MAXN-2,0) niyuan[i]=niyuan[i+1](i+1)%MOD;
s[0][0]=1;
rep(i,1,MAXN-1){
rep(j,1,i){
s[i][j]=(s[i-1][j-1]+js[i-1][j])%MOD;
}
}
rep(i,0,MAXN-1) b[0][i]=1;
rep(i,1,MAXN-1){
rep(j,1,MAXN-1){
b[i][j]=b[i][j-1];
if(i>=j) b[i][j]=(b[i][j]+b[i-j][j])%MOD;
}
}
int sum4=0;
rep(i,1,m) sum4=(sum4+s[n][i])%MOD;
cout<<qpow(m,n)<<endl;
if(n>m) cout<<0<<endl;
else cout<<f[m]niyuan[m-n]%MOD<<endl;
if(n<m) cout<<0<<endl;
else cout<<f[m]*s[n][m]%MOD<<endl;
cout<<sum4<<endl;
if(n<=m) cout<<1<<endl;
else cout<<0<<endl;
if(n<m) cout<<0<<endl;
else cout<<s[n][m]<<endl;
cout<<C(n+m-1,m-1)<<endl;
cout<<C(m,n)<<endl;
if(n<m) cout<<0<<endl;
else cout<<C(n-1,m-1)<<endl;
cout<<b[n][m]<<endl;
if(n<=m) cout<<1<<endl;
else cout<<0<<endl;
if(n<m) cout<<0<<endl;
else cout<<b[n-m][m]<<endl;
return 0;
}
::::
到这里,组合数学初步知识就到此为止了,希望你能有所收获。
附:涉及到本文的模板如下:
::::info[所有模板代码]
#include<bits/stdc++.h>
#define int long long
#define rep(z,x,y) for(int z=x;z<=y;z++)
#define drep(z,x,y) for(int z=x;z>=y;z--)
using namespace std;
const int MAXN = 2000005;
const int MOD = 1e9 + 7;
int fac[MAXN], ifc[MAXN];
// 快速幂
int fpw(int a, int b){
int r = 1;
while(b){
if(b & 1) r = r * a % MOD;
a = a * a % MOD;
b >>= 1;
}
return r;
}
// 预处理阶乘、阶乘逆元,n为最大范围
void init(int n){
fac[0] = 1;
rep(i, 1, n){
fac[i] = fac[i-1] * i % MOD;
}
ifc[n] = fpw(fac[n], MOD - 2);
drep(i, n-1, 0){
ifc[i] = ifc[i+1] * (i+1) % MOD;
}
}
// 组合数 C(n,k)
int C(int n, int k){
if(k < 0 || k > n) return 0;
return fac[n] * ifc[k] % MOD * ifc[n - k] % MOD;
}
// ========== 杨辉三角递推求组合数,n<=2000 ==========
const int MAXY = 2005;
int cyh[MAXY][MAXY];
void initC(int n){
rep(i, 0, n){
cyh[i][0] = cyh[i][i] = 1;
rep(j, 1, i-1){
cyh[i][j] = (cyh[i-1][j-1] + cyh[i-1][j]) % MOD;
}
}
}
// ========== 多重集全排列 ==========
int fac_big(int n){
int r = 1;
rep(i, 2, n) r *= i;
return r;
}
int msp(vector<int>& cnt, int n){
int ans = fac_big(n);
for(int c : cnt){
ans /= fac_big(c);
}
return ans;
}
// ========== 错位排列 D_n ==========
int derange(int n){
const int maxd = 25;
vector<int> f(maxd + 1);
f[1] = 0;
f[2] = 1;
rep(i, 3, n){
f[i] = (i - 1) * (f[i-1] + f[i-2]);
}
return f[n];
}
// ========== 容斥:求1~n中与m互质的数的个数 ==========
int cpc(int n, int m){
vector<int> pr;
int tmp = m;
for(int i = 2; i * i <= tmp; i++){
if(tmp % i == 0){
pr.push_back(i);
while(tmp % i == 0) tmp /= i;
}
}
if(tmp > 1) pr.push_back(tmp);
int k = pr.size();
int ans = n;
for(int ms = 1; ms < (1 << k); ms++){
int bt = __builtin_popcount(ms);
int prod = 1;
rep(i, 0, k-1){
if(ms & (1 << i)) prod *= pr[i];
}
if(bt & 1) ans -= n / prod;
else ans += n / prod;
}
return ans;
}
// ========== 卡特兰数 C_n = 1/(n+1)*C(2n,n) ==========
int cat(int n){
return C(2*n, n) * fpw(n + 1, MOD - 2) % MOD;
}
// ========== 第二类斯特林数 S(n,k) 递推版 ==========
int s2(int n, int k){
if(k < 0 || k > n) return 0;
vector<vector<int>> S(n+1, vector<int>(k+1, 0));
S[0][0] = 1;
rep(i, 1, n){
rep(j, 1, min(i, k)){
S[i][j] = (S[i-1][j-1] + j * S[i-1][j]) % MOD;
}
}
return S[n][k];
}
// 第二类斯特林数 通项公式(需要预处理fac,ifc)
int s2f(int n, int k){
int ans = 0;
rep(i, 0, k){
int tm = C(k, i) * fpw(i, n) % MOD;
if((k - i) & 1) ans = (ans - tm + MOD) % MOD;
else ans = (ans + tm) % MOD;
}
return ans * ifc[k] % MOD;
}
// ========== 第一类无符号斯特林数 c(n,k) ==========
int s1(int n, int k){
if(k < 0 || k > n) return 0;
vector<vector<int>> s(n+1, vector<int>(k+1, 0));
s[0][0] = 1;
rep(i, 1, n){
rep(j, 1, min(i, k)){
s[i][j] = (s[i-1][j-1] + (i-1) * s[i-1][j]) % MOD;
}
}
return s[n][k];
}
// ========== 贝尔数 B_n ==========
int bel(int n){
vector<int> b(n+1);
b[0] = 1;
rep(i, 0, n-1){
b[i+1] = 0;
rep(k, 0, i){
b[i+1] = (b[i+1] + C(i, k) * b[k]) % MOD;
}
}
return b[n];
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
init(2000000);
// 样例调用
// cout << C(5,2) << '\n';
// cout << cat(5) << '\n';
// cout << s2(5,2) << '\n';
// cout << s1(5,2) << '\n';
// cout << bel(5) << '\n';
return 0;
}
::::
感谢你的观看,组合数学初步,至此完。