You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

340 lines
14 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

## 卡特兰数专题($Catalan$
### 一、什么是卡特兰数?
明安图数,又称卡塔兰数,英文名$Catalan$ $number$,是**组合数学**中一个常出现于各种计数问题中的数列。以中国蒙古族数学家明安图 $(1692-1763)$和比利时的数学家欧仁·查理·卡塔兰 $(18141894)$的名字来命名,其前几项为(从第零项开始) : $1, 1, 2, 5, 14, 42, 132, 429, 1430, 4862,16796, 58786 …$
#### 知乎 卡特兰数四连击
https://zhuanlan.zhihu.com/p/31317307
https://zhuanlan.zhihu.com/p/31526354
https://zhuanlan.zhihu.com/p/31585260
https://zhuanlan.zhihu.com/p/31050581
**[嘉持老师的优秀教学视频](https://www.bilibili.com/video/BV1nE411A7ST)**
#### 卡特兰数的几何意义
简单来说,卡特兰数就是一个有规律的数列,在坐标图中可以表示为:从原点$(0,0)$出发,每次向$x$轴或者$y$轴正方向移动$1$个单位,直到到达$(n,n)$点,且在移动过程中不越过第一象限平分线的移动方案总数。
<center><img src='https://img-blog.csdnimg.cn/20210203164401967.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2NoZW5sb25nX2N4eQ==,size_16,color_FFFFFF,t_70'></center>
### 卡特兰数公式推导
我们暂且先不考虑移动过程中不越过第一象限平分线这个约束条件,那么从$(0,0)$点到$(n,n)$点的过程中,我们总共需要向右移动$n$步,向上移动$n$步,一共$2n$步。我们可以理解为在$2n$步里面选出$n$步来向上移动,那么剩下的$n$步就是向右移动的步数,那么方案总数就是$\LARGE \displaystyle C_{2n}^{n}$
现在,我们来看看如何解决不越过第一象限平分线这个问题。仔细想想,不越过第一象限平分线也就等价于不触碰到$y = x + 1$这条直线。而我们如果把触碰到了直线$y = x + 1$的路线的第一个与$y = x + 1$的触碰点之后的路线关于直线$y = x + 1$对称,并画出对称后的路线
<center><img src='https://img-blog.csdnimg.cn/20210203172545595.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2NoZW5sb25nX2N4eQ==,size_16,color_FFFFFF,t_70'></center>
> <font color='red' size=4><b>黄海解读</b></font>
> ![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/202311101428095.png)
我们会发现触碰到了直线$y = x + 1$的路径的终点都变成了点$(n-1,n+1)$。也就是说,从$(0,0)$点到$(n,n)$点的路线当中触碰了直线$y = x + 1$的路线条数与从$(0,0)$点到$(n-1,n+1)$点的路线条数的数量是**相等**的。于是从$(0,0)$点到$(n,n)$点的非法路径条数为$\LARGE \displaystyle C_{2n}^{n-1}$ 。
综上所述,从$(0,0)$点到$(n,n)$点满足条件的路径数为
<h2><font color='red'>卡特兰数通项公式I</font></h2></
<font color='red'>$$\huge f(n)=C_{2n}^{n}-C_{2n}^{n-1}$$</font>
---
通过化简,公式可以简化为:
$$\large Catalan_n= C_{2n}^{n}-C_{2n}^{n-1}=\frac{(2n)!}{n! \times n!}-\frac{(2n)!}{(n+1)!\times (n-1)!} \\ =\frac{(2n)!\times (n+1)}{n!\times (n+1)!}-\frac{(2n)!\times n}{n! \times (n+1)!} \\ =\frac{(2n)!\times (n+1)-(2n)!\times n}{n!\times (n+1)!} \\ =\frac{(2n)!}{n!\times (n+1)!}=\frac{1}{n+1}\times \frac{(2n)!}{n!\times n!} =\frac{C_{2n}^n}{n+1}
$$
<h2><font color='red'>卡特兰数通项公式II</font></h2>
<font color='red'>$$\huge \displaystyle f(n)=\frac{C_{2n}^n}{n+1} $$</font>
---
除了这个通项公式之外,卡特兰数还有一个由该通项公式推导而来的递推公式:
$\large \displaystyle Catalan_{n+1}= \frac{1}{n+2}C_{2n+2}^{n+1} \\
=\frac{1}{n+2}\times \frac{(2n+2)!}{(n+1)!\times (n+1)!} \\
=\frac{1}{n+2}\times \frac{(2n)!\times (2n+1) \times (2n+2)}{n! \times n!\times (n+1)^2} \\
=\frac{1}{n+2}\times \frac{(2n+1)\times(2n+2)}{n+1} \times \frac{1}{n+1} \times \frac{(2n)!}{n!\times n!} \\
=\frac{2(2n+1)}{n+2}\times \frac{1}{n+1}\times C_{2n}^n \\
=\frac{4n+2}{n+2}Catalan_n
$
**初始值:`f[0] = f[1] = 1`**
<h2><font color='red'>卡特兰数公式III(递推)</font></h2>
<font color='red'>$$\huge f(n)=\frac{4n-2}{n+1}f(n-1) $$</font>
<h2><font color='red'>卡特兰数公式IV(取模常用)</font></h2>
<font color='red'>$$ \huge f(n)=\frac{(2n)!}{(n+1)! \times n!} $$</font>
<h2><font color='red'>卡特兰数公式V(递推)</font></h2>
<h2><font color='red'>一般不用来实现,用来推规律</font></h2>
<font color='red'>$$\huge \displaystyle f(n)=\sum_{i=0}^{n-1}f(i)f(n-i-1) $$</font>
证明:在$n$对括号的排列中,假设最后一个括号和第$i$个左括号匹配。则在第$i$个左括号之前,一定已经匹配上了($i-1$)对左括号。如下图,因此,此种情况的数量为$f(i-1)*f(n-i)$。($1<=i<=n$)最后一个右括号可以$1\sim n$个左括号匹配共$n$种情况。
<center><img src='https://img-blog.csdnimg.cn/20200410171634180.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDUyMDg4MQ==,size_16,color_FFFFFF,t_70'></center>
因此,对$i$从$1$到$n$的情况求和得到$\large \displaystyle f(n)=\sum_{i=0}^{n-1}f(i)f(n-i-1)$,即可得到递推公式。
### 二、常见考点
#### 1、 进出栈问题
设栈$S$的初始状态为空,元素$a,b,c,d,e$**依次**入栈,以下**出栈序列**有多少种**可能性**<font color='red'><b>注意:这个序列顺序是定的.</b></font>
重点:归纳法思考,由大及小。
我们这样去想,假设最终的出栈序列可能性用$f(n)$表示,其中$n$就是元素的个数。
假设第$k$个数是最后出栈的数,那么:
* 比它小的前$k-1$个数,肯定已经完成了入栈,出栈操作。因为从逻辑顺序上来讲,它们无法压到$k$下面去吧。
* 比它大的后$n-k$个数,肯定已经完成了入栈,出栈操作。它们倒是可以压到$k$下面去,但假设$k$是最后一个出栈的,它们不能破坏掉假设,也必须提出出栈。
现在,$k$将原来的问题,划分为两个子问题$f(n-k)$和$f(k-1)$,根据乘法原理,结果就是$f(n-k)*f(k-1)$。
$k$的取值范围是$1 \leq k \leq n$,再根据加法原理:
$$\large f(n)=\sum_{k=1}^{n}f(n-k)\times f(k-1)$$
展开写就是:$$\large f(n)=f(0) \times f(n-1) + f(1) \times f(n-2) + ... +f(n-1)\times f(0)$$
代码实现:
```cpp {.line-numbers}
f[0] = 1;
for (int i = 1; i <= 12; i++) {
int fi = 0;
for (int j = 0; j <= i - 1; j++) fi += f[j] * f[i - j - 1];
cout << fi << " ";
}
```
[P1044 [NOIP2003 普及组] 栈](https://www.luogu.com.cn/problem/P1044)
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 20;
int f[N];
int main() {
int n;
cin >> n;
f[0] = 1;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= i; j++)
f[i] += f[j - 1] * f[i - j];
cout << f[n] << endl;
return 0;
}
```
#### 2、排队问题
变种(排队问题):出栈入栈问题有许多的变种,比如$n$个人拿$5$元、$n$个人拿$10$元买物品,物品$5$元,老板没零钱。问有几种排队方式。熟悉栈的同学很容易就能把这个问题转换为栈。值得注意的是,由于每个拿$5$元的人排队的次序不是固定的,所以最后求得的答案要$n!$。拿$10$元的人同理,所以还要$n!$。所以这种变种的最后答案为$h(n)*n!$。
[P1754 球迷购票问题](https://www.luogu.com.cn/problem/P1754)
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 30;
LL f[N];
int main() {
int n;
cin >> n;
f[0] = 1;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= i; j++)
f[i] += f[j - 1] * f[i - j];
cout << f[n] << endl;
return 0;
}
```
#### 3、 二叉树构成问题
有$n$个结点,问总共能构成几种不同的二叉树。
我们可以假设,如果采用中序遍历的话,根结点第$k$个被访问到,则根结点的左子树有$k-1$个点、根结点的右指数有$n-k$个点。$k$的取值范围为$1$到$n$。讲到这里就很明显看得出是卡特兰数了。
[AcWing 1645. 不同的二叉搜索树](https://www.acwing.com/solution/content/35252/)
没有超过$long$ $long$的存储范围的话,可以使用递推;
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1010, MOD = 1e9 + 7;
int n;
int f[N];
int main() {
cin >> n;
f[0] = 1;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= i; j++)
f[i] = (f[i] + (LL)f[j - 1] * f[i - j]) % MOD;
cout << f[n] << endl;
return 0;
}
```
[AcWing 1317. 树屋阶梯](https://www.acwing.com/problem/content/description/1319/)
模拟,按照公式(1)进行模拟,需要使用高精度
[黄海的题解](https://www.cnblogs.com/littlehb/p/16365245.html)
[AcWing 1257. 二叉树计数](https://www.acwing.com/problem/content/1259/)
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 10010; // 5000*2+10
int primes[N], cnt;
bool st[N];
int a[N], b[N];
void get_primes(int n) {
for (int i = 2; i <= n; i++) {
if (!st[i]) primes[cnt++] = i;
for (int j = 0; primes[j] * i <= n; j++) {
st[i * primes[j]] = true;
if (i % primes[j] == 0) break;
}
}
}
int get(int n, int p) { // n!中p的次数
int s = 0;
while (n) n /= p, s += n;
return s;
}
void mul(int a[], int b, int &len) {
int t = 0;
for (int i = 1; i <= len; i++) {
t += a[i] * b;
a[i] = t % 10;
t /= 10;
}
while (t) {
a[++len] = t % 10;
t /= 10;
}
//去前导0
while (len > 1 && !a[len]) len--;
}
int C(int a, int b, int c[]) {
int len = 1;
c[1] = 1;
for (int i = 0; i < cnt; i++) {
int p = primes[i];
int s = get(a, p) - get(b, p) - get(a - b, p);
while (s--) mul(c, p, len);
}
return len;
}
void sub(int a[], int b[], int &len) {
int t = 0;
for (int i = 1; i <= len; i++) {
t = a[i] - b[i] - t;
a[i] = (t + 10) % 10;
t < 0 ? t = 1 : t = 0;
}
while (len > 1 && !a[len]) len--;
}
int main() {
//加快读入
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
get_primes(N - 10);
int n;
cin >> n;
int a1 = C(n + n, n, a);
int b1 = C(n + n, n - 1, b); // bl下面没有用到原因是两数相减我们知道a>b,按着a的长度来就行了
sub(a, b, a1);
for (int i = a1; i >= 1; i--) printf("%d", a[i]);
return 0;
}
```
#### 4、$01$序列
给出一个$n$,要求一个长度为$2n$的$01$序列,使得序列的任意前缀中$1$的个数不少于$0$的个数,有多少个不同的$01$序列?
以下为长度为$6$的序列:
`111000 101100 101010 110010 110100`
类比一下括号问题,此题就是一个祼的卡特兰数问题。
#### 5、$+1$ $-1$序列
$n$个$+1$和$n$个$-1$构成的$2n$项 $a_1,a_2,···,a_{2n}$ ,其部分和满足非负性质,即$a_1+a_2+···+a_k>=0(k=1,2,···,2n)$ ,有多少个不同的此序列?
此典例解析与$01$序列解析一模一样,即此数列的个数等于第$n$个$Catalan$数,此处就不再赘述。
#### 6、凸多边形划分
在一个$n$边形中,通过不相交于$n$边形内部的对角线,把$n$边形拆分为若干个三角形,问有多少种拆分方案?
如五边形有如下$5$种拆分方案:
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/202311101459139.png)
如六边形有如下14种拆分方案
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/202311101504741.png)
> **结论**:对凸$n$边形进行不同的三角形分割(只连接顶点对形成$n$个三角形)数为$h[n-2]$
这也是非常经典的一道题。我们可以这样来看,选择一个 **基边** ,显然这是多边形划分完之后某个三角形的一条边。图中我们假设基边是$p_1,p_n$,我们就可以用 $p_1,p_n$ 和另外一个点假设为$p_i$做一个三角形,并将多边形分成三部分(要是$i$与$1,n$挨着的话,就是两部分),除了中间的三角形之外,一边是$i$边形,另一边是$ni+1$边形。$i$的取值范围是$2$到$n1$。所以本题的解
$$\large c(n)=c(2)*c(n1)+c(3)*c(n2)+...c(n1)*c(2)$$
令$f(i)=c(i+2)$则
$$\large f(i)=f(0)f(i1)+f(1)f(i2)...+f(i1)f(0)$$
很明显,这就是一个卡特兰数了。
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/202311101517805.png)
### [四、链接资源](https://blog.csdn.net/Sherry_Yue/article/details/88364746)
### 五、递推与递归的代码实现
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 10;
int g[N];
// 递归
int f(int n) {
if (n == 0 || n == 1) return 1;
int sum = 0;
// f(0)*f(n-1)+f(1)*f(n-2)+....+f(n-1)*f(0)
for (int i = 0; i < n; i++)
sum += f(i) * f(n - 1 - i);
return sum;
}
int main() {
// 1、递推法
g[0] = g[1] = 1;
for (int i = 2; i < N; i++)
for (int j = 0; j < i; j++)
g[i] += g[j] * g[i - j - 1]; // 考虑思路:画括号法
for (int i = 0; i < N; i++)
cout << g[i] << endl;
// 2 、递归法
for (int i = 0; i < N; i++)
cout << f(i) << endl;
return 0;
}
```