浅析卡特兰数
1.卡特兰数是什么
卡特兰数(英语:Catalan number),又称卡塔兰数、明安图数,是组合数学中一种常出现于各种计数问题中的数列。以比利时数学家欧仁·查理·卡特兰的名字命名。
1730年,清代蒙古族数学家明安图在对三角函数幂级数的推导过程中首次发现,1774年被发表在《割圜密率捷法》。
卡特兰数的第
i
i
i项我们记为
C
i
C_i
Ci,注意:不是组合数学中的那个
C
n
m
C^m_n
Cnm,我们要分清楚
C
n
m
和
C
i
C^m_n和C_i
Cnm和Ci的区别,其实你只要知道卡特兰数只有下标,但组合数有上标和下标就好了
卡特兰数的下标是从
0
0
0开始的,它的前几项为:1,1,2,5,14,42…
2.卡特兰数能解决的问题
卡特兰数能解决的问题有很多,如:
进出栈序列
括号序列
二叉树的一系列问题
…
本文将从这些问题中带你推出卡特兰数的通项公式和递推公式,并且解决几道题目
3.解决题目,推出公式
题目一:进出栈序列
注意:这时候可能会设计到栈的知识,可以去参考liyuxuan的单调队列笔记中对栈的描述
oj原题,luogu原题,Acwing原题
三个地方的数据范围都不同,oj和luogu的数据范围都是
1
≤
n
≤
18
1\le n\le 18
1≤n≤18,但Acwing上的数据范围是
1
≤
n
≤
6
×
1
0
4
1\le n \le 6\times10^4
1≤n≤6×104
所以我们就先讨论oj和luogu上的题目怎么去做
卡特兰数其实对于我来说有两种公式,第一种是
O
(
n
2
)
O(n^2)
O(n2)做法的公式,第二种是
O
(
n
)
O(n)
O(n)做法的公式
我们先去讨论这道题不用卡特兰数的做法
做法一:递归(搜索),
O
(
2
n
)
O(2^n)
O(2n)
我们可以考虑用递归来枚举每一种状态,对于每种状态,它可以衍生出两种状态,一:将一个数进栈,二:将栈顶出栈
所以我们就可以给
d
f
s
dfs
dfs函数设置两个参数,一个是还没有进入栈的有多少个元素和栈内现在有多少个元素
所以可以定义
d
f
s
(
i
,
j
)
dfs(i,j)
dfs(i,j)的意思是还有i个元素没有进入和j个数未出栈的总方案
这样写起来应该会写吧,主函数内调用的就是
d
f
s
(
n
,
0
)
dfs(n,0)
dfs(n,0),意思是n个数还没有进栈和0个数没有出栈的总方案
所以代码奉上:
//注意,这种方法在三个网站上都过不了,应为他的时间复杂度太高了
#include<iostream>
using namespace std;
int dfs(int i,int j){
if(i==0)return 1;//没有数要进栈,方案数为1
int sum=0;
if(j>0)sum+=dfs(i,j-1);//将一个数出栈
sum+=dfs(i-1,j+1);//将一个数进栈
return sum;
}
int main(){
int n;
cin>>n;
cout<<dfs(n,0);
return 0;
}
做法二:在递归的基础上使用记忆化搜索/DP,
O
(
n
2
)
O(n^2)
O(n2)
不会记忆化搜索和DP的同学在网上搜
记忆化搜索:
//注意:这种方法只在oj和luogu上过的了,在Acwing上会爆时间
#include<iostream>
using namespace std;
const int N=20;
int f[N][N];
int dfs(int i,int j){
if(f[i][j])return f[i][j];//如果已经计算过了
if(i==0)return 1;//没有数要进栈,方案数为1
if(j>0)f[i][j]+=dfs(i,j-1);//将一个数出栈
f[i][j]+=dfs(i-1,j+1);//将一个数进栈
return f[i][j];
}
int main(){
int n;
cin>>n;
cout<<dfs(n,0);
return 0;
}
DP在这里就不写了,大家自行在网上去查找(其实是作者不知道怎么写)
好了,这里就是我目前知道的这道题不用卡特兰数的做法,如果你有其他的做法,可以发出来分享
接下来讲解卡特兰数做法
做法一:卡特兰数
O
(
n
2
)
O(n^2)
O(n2)递推公式
好像卡特兰数
O
(
n
2
)
O(n^2)
O(n2)没有通项公式吧,反正作者不知道
好了,这个时候就有那么亿点点烧脑了
我们设
f
i
为要进入
i
个数的时候的方案总数
我们将第
i
个数进入记作
(
i
,
+
1
)
将第
i
个数弹出记作
(
i
,
−
1
)
这样,我们就可以写出一个进出栈序列
比如说
:
当
n
=
3
的时候
进出栈序列可能是
(
1
,
+
1
)
(
2
,
+
1
)
(
3
,
+
1
)
(
3
,
−
1
)
(
2
,
−
1
)
(
1
,
−
1
)
还有可能是
:
(
1
,
+
1
)
(
1
,
−
1
)
(
2
,
+
1
)
(
2
,
−
1
)
(
3
,
+
1
)
(
3
,
−
1
)
并且这个序列的长度一定为
2
n
那么我们可以将
f
n
的定义写成
:
构造长度为
2
n
的进出栈序列的方案总数
那么
(
1
,
+
1
)
的位置肯定是在第一个
但
(
1
,
−
1
)
就不知道了
我们设
(
1
,
−
1
)
在第
k
+
2
个位置
那么在
(
1
,
+
1
)
到
(
1
,
−
1
)
中肯定会有
2
到
k
/
2
个数的进栈和出栈
因为要在第
k
+
2
个位置出栈必须要等
2
到
k
/
2
+
1
个数出栈后才能出栈
,
这是栈的特性
那么这
k
/
2
个数的进出栈顺序又可以写成
f
k
/
2
那么我刚才说过这个序列的长度为
2
n
那么
(
1
,
−
1
)
后面的长度就为
2
n
−
k
−
2
那么后面的东西一定是
k
/
2
+
2
到
n
的进出栈序列
所以说可以写成
f
(
2
n
−
k
−
2
)
/
2
,
也就是
f
n
−
k
/
2
−
1
两个数
(
f
k
和
f
n
−
k
/
2
−
1
)
具有乘法原理,也就是说当
(
1
,
−
1
)
的位置在第
k
+
2
位时,总方案数为
f
k
/
2
f
n
−
k
/
2
−
1
因此,我们可以对于每个
f
i
枚举所有的
k
,从而推出每个
f
i
虽然这样已经可以推出所有的
f
i
了,但是我们为了简洁,可以去枚举所有的
k
/
2
所以我们对于每个
k
,
i
=
k
/
2
,
那么递推公式就变成了
f
i
f
n
−
i
−
1
原来的
k
是要
≤
2
n
−
2
的,现在换成
i
之后就变成了
≤
n
−
1
于是我们就得到了
f
递推公式
我们还要注意一点
,
f
0
=
1
,
因为长度为
0
的进出栈序列有
f
n
=
∑
i
=
0
n
−
1
f
i
f
n
−
i
−
1
好了,这个时候就有那么亿点点烧脑了\\ 我们设f_i为要进入i个数的时候的方案总数\\ 我们将第i个数进入记作(i,+1)\\ 将第i个数弹出记作(i,-1)\\ 这样,我们就可以写出一个进出栈序列\\ 比如说:当n=3的时候\\ 进出栈序列可能是(1,+1)(2,+1)(3,+1)(3,-1)(2,-1)(1,-1)\\ 还有可能是:(1,+1)(1,-1)(2,+1)(2,-1)(3,+1)(3,-1)\\ 并且这个序列的长度一定为2n\\ 那么我们可以将f_n的定义写成:构造长度为2n的进出栈序列的方案总数\\ 那么(1,+1)的位置肯定是在第一个\\ 但(1,-1)就不知道了\\ 我们设(1,-1)在第k+2个位置\\ 那么在(1,+1)到(1,-1)中肯定会有2到k/2个数的进栈和出栈\\ 因为要在第k+2个位置出栈必须要等2到k/2+1个数出栈后才能出栈,这是栈的特性\\ 那么这k/2个数的进出栈顺序又可以写成f_{k/2}\\ 那么我刚才说过这个序列的长度为2n\\ 那么(1,-1)后面的长度就为2n-k-2\\ 那么后面的东西一定是k/2+2到n的进出栈序列\\ 所以说可以写成f_{(2n-k-2)/2},也就是f_{n-k/2-1}\\ 两个数(f_{k}和f_{n-k/2-1})具有乘法原理,也就是说当(1,-1)的位置在第k+2位时,总方案数为f_{k/2}f_{n-k/2-1}\\ 因此,我们可以对于每个f_i枚举所有的k,从而推出每个f_i\\ 虽然这样已经可以推出所有的f_i了,但是我们为了简洁,可以去枚举所有的k/2\\ 所以我们对于每个k,i=k/2,那么递推公式就变成了f_{i}f_{n-i-1}\\ 原来的k是要\le 2n-2的,现在换成i之后就变成了\le n-1\\ 于是我们就得到了f递推公式\\ 我们还要注意一点,f_0=1,因为长度为0的进出栈序列有\\ f_n=\sum_{i=0}^{n-1}f_{i}f_{n-i-1}
好了,这个时候就有那么亿点点烧脑了我们设fi为要进入i个数的时候的方案总数我们将第i个数进入记作(i,+1)将第i个数弹出记作(i,−1)这样,我们就可以写出一个进出栈序列比如说:当n=3的时候进出栈序列可能是(1,+1)(2,+1)(3,+1)(3,−1)(2,−1)(1,−1)还有可能是:(1,+1)(1,−1)(2,+1)(2,−1)(3,+1)(3,−1)并且这个序列的长度一定为2n那么我们可以将fn的定义写成:构造长度为2n的进出栈序列的方案总数那么(1,+1)的位置肯定是在第一个但(1,−1)就不知道了我们设(1,−1)在第k+2个位置那么在(1,+1)到(1,−1)中肯定会有2到k/2个数的进栈和出栈因为要在第k+2个位置出栈必须要等2到k/2+1个数出栈后才能出栈,这是栈的特性那么这k/2个数的进出栈顺序又可以写成fk/2那么我刚才说过这个序列的长度为2n那么(1,−1)后面的长度就为2n−k−2那么后面的东西一定是k/2+2到n的进出栈序列所以说可以写成f(2n−k−2)/2,也就是fn−k/2−1两个数(fk和fn−k/2−1)具有乘法原理,也就是说当(1,−1)的位置在第k+2位时,总方案数为fk/2fn−k/2−1因此,我们可以对于每个fi枚举所有的k,从而推出每个fi虽然这样已经可以推出所有的fi了,但是我们为了简洁,可以去枚举所有的k/2所以我们对于每个k,i=k/2,那么递推公式就变成了fifn−i−1原来的k是要≤2n−2的,现在换成i之后就变成了≤n−1于是我们就得到了f递推公式我们还要注意一点,f0=1,因为长度为0的进出栈序列有fn=i=0∑n−1fifn−i−1
我为什么要说这种做法和卡特兰数有关呢?
因为你将这个序列打印出来之后就会发现它其实就是卡特兰数序列
我们刚刚推出的那个公式就是卡特兰数
O
(
n
2
)
O(n^2)
O(n2)的递推公式
所以我们就可以开始写代码啦
//注意:这种方法只在oj和luogu上过的了,在Acwing上会爆时间
#include<iostream>
using namespace std;
const int N=20;
int f[N];
int main(){
int n;
cin>>n;
f[0]=1;
for(int i=1;i<=n;i++){//循环计算每一个f[i]
for(int j=0;j<n;j++){//根据刚才的公式写的
f[i]+=f[j]*f[i-j-1];//乘法原理
}
}
cout<<f[n];
return 0;
}
有人可能会问:wxy,为什么老是过不了Acwing上面的题目
你去想:目前最好的方法时间复杂度也就
O
(
n
2
)
O(n^2)
O(n2),Acwing上的极限数据是
6
×
1
0
4
6\times10^4
6×104,
n
2
n^2
n2的话是
6
×
1
0
4
×
6
×
1
0
4
=
36
×
1
0
8
=
3.6
×
1
0
9
6\times10^4\times6\times10^4=36\times10^8=3.6\times10^9
6×104×6×104=36×108=3.6×109
这不立马就爆啦
而且他的数据还要开高精,所以过不了
所以我们现在要开始推卡特兰数
O
(
n
)
O(n)
O(n)的通项公式和递推公式啦
做法二:卡特兰数
O
(
n
)
O(n)
O(n)通项公式
如果你不知道组合数是什么,可以去参考吴浩宁写的组合数学
终极烧脑
,
启动
!
!
前置知识
:
组合数
,
组合数公式
组合数公式
:
C
n
m
=
n
!
m
!
(
n
−
m
)
!
,
′
!
′
表示阶乘
,
n
!
=
n
×
(
n
−
1
)
×
(
n
−
2
)
×
.
.
.
×
2
×
1
上面我们定义了进出栈序列,现在我们将进出栈序列重新定义一下
把每个元素只设成
+
1
或
−
1
为什么可以这么写
因为进栈顺序一定是
1
到
n
并且栈顶一定是唯一的
所以可以只用
+
1
或
−
1
代替
我们拿上面的例子来举例
当
n
=
3
的时
原来的进出栈序列
:
(
1
,
+
1
)
(
2
,
+
1
)
(
3
,
+
1
)
(
3
,
−
1
)
(
2
,
−
1
)
(
1
,
−
1
)
改进后的进出栈序列
:
+
1
+
1
+
1
−
1
−
1
−
1
原来的进出栈序列
:
(
1
,
+
1
)
(
1
,
−
1
)
(
2
,
+
1
)
(
2
,
−
1
)
(
3
,
+
1
)
(
3
,
−
1
)
改进后的进出栈序列
:
+
1
−
1
+
1
−
1
+
1
−
1
两种序列的意思还是一样的
好了,现在我们要来定义这个序列是合法还是不合法
如果栈按照进出栈序列可以正常运行,那么我们称这个序列是合法的
否则,这个序列就不合法
并且合法的长度为
2
n
的进出栈序列一定有
n
个
+
1
和
n
个
−
1
比如,当
n
=
1
时
−
1
+
1
这个序列时不合法的
因为最开始时,栈里面是空的,不能弹出东西了
+
1
−
1
这个序列时合法的
其实我们可以得出一个结论
对于一个进出栈序列
A
对于每一个位置的位置
i
,
如果
0
≤
∑
j
=
1
i
A
j
那么这个序列就是合法的
比如说我们可以哪一个序列来举例
终极烧脑,启动!!\\ 前置知识:组合数,组合数公式\\ 组合数公式:C^m_n=\frac{n!}{m!(n-m)!},'!'表示阶乘,n!=n\times(n-1)\times(n-2)\times...\times2\times1\\ 上面我们定义了进出栈序列,现在我们将进出栈序列重新定义一下\\ 把每个元素只设成+1或-1\\ 为什么可以这么写\\ 因为进栈顺序一定是1到n\\ 并且栈顶一定是唯一的\\ 所以可以只用+1或-1代替\\ 我们拿上面的例子来举例\\ 当n=3的时\\ 原来的进出栈序列:(1,+1)(2,+1)(3,+1)(3,-1)(2,-1)(1,-1)\\ 改进后的进出栈序列:+1 +1 +1 -1 -1 -1\\ 原来的进出栈序列:(1,+1)(1,-1)(2,+1)(2,-1)(3,+1)(3,-1)\\ 改进后的进出栈序列:+1 -1 +1 -1 +1 -1\\ 两种序列的意思还是一样的\\ 好了,现在我们要来定义这个序列是合法还是不合法\\ 如果栈按照进出栈序列可以正常运行,那么我们称这个序列是合法的\\ 否则,这个序列就不合法\\ 并且合法的长度为2n的进出栈序列一定有n个+1和n个-1 比如,当n=1时\\ -1 +1\\ 这个序列时不合法的\\ 因为最开始时,栈里面是空的,不能弹出东西了\\ +1 -1\\ 这个序列时合法的\\ 其实我们可以得出一个结论\\ 对于一个进出栈序列A\\ 对于每一个位置的位置i,如果0 \le \sum_{j=1}^{i}A_j\\ 那么这个序列就是合法的\\ 比如说我们可以哪一个序列来举例\\
终极烧脑,启动!!前置知识:组合数,组合数公式组合数公式:Cnm=m!(n−m)!n!,′!′表示阶乘,n!=n×(n−1)×(n−2)×...×2×1上面我们定义了进出栈序列,现在我们将进出栈序列重新定义一下把每个元素只设成+1或−1为什么可以这么写因为进栈顺序一定是1到n并且栈顶一定是唯一的所以可以只用+1或−1代替我们拿上面的例子来举例当n=3的时原来的进出栈序列:(1,+1)(2,+1)(3,+1)(3,−1)(2,−1)(1,−1)改进后的进出栈序列:+1+1+1−1−1−1原来的进出栈序列:(1,+1)(1,−1)(2,+1)(2,−1)(3,+1)(3,−1)改进后的进出栈序列:+1−1+1−1+1−1两种序列的意思还是一样的好了,现在我们要来定义这个序列是合法还是不合法如果栈按照进出栈序列可以正常运行,那么我们称这个序列是合法的否则,这个序列就不合法并且合法的长度为2n的进出栈序列一定有n个+1和n个−1比如,当n=1时−1+1这个序列时不合法的因为最开始时,栈里面是空的,不能弹出东西了+1−1这个序列时合法的其实我们可以得出一个结论对于一个进出栈序列A对于每一个位置的位置i,如果0≤j=1∑iAj那么这个序列就是合法的比如说我们可以哪一个序列来举例
值 | +1 | -1 | -1 | +1 |
---|---|---|---|---|
i(下标) | 1 | 2 | 3 | 4 |
$$ | ||||
对于位置3\ | ||||
它的前缀和为(+1)+(-1)+(-1)=-1\ | ||||
-1\le 0\ | ||||
所以说这个序列不合法\ | ||||
现在知道什么是合法序列了吧\ | ||||
那么我们现在先要知道不考虑合法不合法的情况下,含有n个+1和n个-1的序列有多少种\ | ||||
答案是C^{n}_{2n}种\ | ||||
为什么呢?\ | ||||
想知道为什么,就要了解组合数的定义\ | ||||
C^m_n的意思是从n个数中取出m个数,而且取出的数和之前取出的不一样,那么他的总取法有多少种\ | ||||
那么C^n_{2n}呢?\ | ||||
从2n个数中取出n个数,而且取出的数和之前取出的不一样\ | ||||
将取出的数全变成+1,那么没有取出的数就全是-1,这样取出的总方案就是C^n_{2n}的意思\ | ||||
接下来,由于我们要求出合法序列的总方案\ | ||||
那我们是不是可以用,n个+1和n个-1的序列总数-非法序列总数就是合法序列总数了\ | ||||
那么接下来我们就要去算非法序列总数有多少种\ | ||||
对于一个非法序列A\ | ||||
A的长度为2n,n=2\ | ||||
A=+1 -1 -1 +1\ | ||||
我们将A的第一个前缀和小于0的地方的下标称为k\ | ||||
将k前面(包括k)的所有地方全部变成那个数的相反数\ | ||||
我们将做完这个操作的序列称作B\ | ||||
B=-1 +1 +1 +1\ | ||||
你会发现B里面有n-1个-1,n+1个+1\ | ||||
我们可以提出一个假设\ | ||||
对于一个非法序列A都有一个对应的B,并且这个B它只会被A得到,而且每个B有n+1个+1和n-1个-1\ | ||||
首先我们要证明’对于一个非法序列A都有一个对应的B,并且这个B它只会被A得到’\ | ||||
A变成B是找第一个前缀和小于0的地方\ | ||||
那么反过来,B变成A不就找第一个前缀和大于0的地方嘛\ | ||||
我们假设序列C也可以得到序列B\ | ||||
那么序列B肯定就不能变成序列C或序列A了,因为B只有一个前缀和小于0的地方\ | ||||
所以说这个证明完成了\ | ||||
第二个证明’每个B有n+1个+1和n-1个-1’\ | ||||
这个很简单\ | ||||
设A的第一个前缀和小于0的地方的下标为k\ | ||||
再设从1到k中有l个+1\ | ||||
则从1到k中有k-l个-1\ | ||||
并且这里有一个结论,k-l=l+1\ | ||||
为什么呢?\ | ||||
因为在第一个前缀和小于0的位置,一定是-1的个数大于+1\ | ||||
并且-1的个数不可能是比+1多大于1的数\ | ||||
不然这个地方就不是第一个前缀和小于0的地方了\ | ||||
所以说这个结论是正确的\ | ||||
于是我们可以把这个公式变形\ | ||||
k-l=l+1\ | ||||
k=2l+1\ | ||||
k-2l=1\ | ||||
这个公式待会会用到\ | ||||
我们想一下\ | ||||
把A变成B后\ | ||||
相当于l就变成了k-l\ | ||||
那么原来A的+1的数量为n\ | ||||
B的+1的数量就为\ | ||||
n-l+(k-l),意思为将A的+1的数量减去k里面的+1数量,加上k里面的-1\ | ||||
我们将这个式子变形\ | ||||
n-l+(k-l)\ | ||||
=n-l+k-l\ | ||||
=n-2l+k\ | ||||
=n+(k-2l)\ | ||||
我们有知道k-2l=1\ | ||||
所以\ | ||||
n+(k-2l) | ||||
=n+1\ | ||||
所以说B的+1数量为n+1\ | ||||
那么B的长度为2n\ | ||||
所以B的-1数量为2n-(n+1)=n-1\ | ||||
所以我们就把上面两个结论都给完成了\ | ||||
那么我们是不是只要推出B的数量,就可以推出所有非法序列A的数量\ | ||||
因为B有n+1个+1,所以他的数量为C{n+1}_{2n}或C{n-1}_{2n},意思和之前的一样\ | ||||
所以根据上面的公式,n个+1和n个-1的序列总数-非法序列总数=合法序列总数=Cn_{2n}-C{n+1}_{2n}\ | ||||
然后根据组合数公式继续变形\ | ||||
Cn_{2n}-C{n+1}_{2n}\ | ||||
=\frac{(2n)!}{n!(2n-n)!}-\frac{(2n)!}{(2n-(n+1))!(n+1)!}\ | ||||
=\frac{(2n)!}{n!n!}-\frac{(2n)!}{(n-1)!(n+1)!}\ | ||||
统一分母\ | ||||
=\frac{(2n)!}{n!n!}-\frac{(2n)!}{(n-1)!n!(n+1)}\ | ||||
=\frac{(2n)!}{n!n!}-\frac{(2n)!n}{n!n!(n+1)}\ | ||||
=\frac{(2n)!}{n!n!}-\frac{\frac{(2n)!n}{n+1}}{n!n!}\ | ||||
=\frac{(2n)!-\frac{(2n)!n}{n+1}}{n!n!}\ | ||||
=\frac{1}{n!n!}((2n)!-\frac{(2n)!n}{n+1})\ | ||||
=\frac{1}{n!n!}(\frac{(2n)!(n+1)}{n+1}-\frac{(2n)!n}{n+1})\ | ||||
=\frac{1}{n!n!}(\frac{(2n)!(n+1)}{n+1}-\frac{(2n)!n}{n+1})\ | ||||
=\frac{1}{n!n!}(\frac{(2n)!(n+1)-(2n)!n}{n+1})\ | ||||
=\frac{1}{n!n!}(\frac{(2n)!((n+1)-n)}{n+1})\ | ||||
=\frac{1}{n!n!}(\frac{(2n)!}{n+1})\ | ||||
=\frac{(2n)!}{n!n!(n+1)}\ | ||||
=\frac{(2n)!}{n!n!}\frac{1}{n+1}\ | ||||
=C^n_{2n}\frac{1}{n+1}\ | ||||
=\frac{C^n_{2n}}{n+1}\ | ||||
然后你去计算这个的值\ | ||||
你会发现和前面的值一摸一样\ | ||||
那么我现在告诉你,你已经推出了卡特兰数的通项公式\ | ||||
也就是说\ | ||||
C_n=\frac{C^n_{2n}}{n+1}\ | ||||
这样我们就可以过题了 | ||||
$$ | ||||
代码: |
//注意,这种方法会爆long long请开高精度后再使用
#include<iostream>
using namespace std;
const int N=20;
long long jc(int n){
if(n<2)return 1;
long long j=1;
for(int i=2;i<=n;i++){
j*=i;
}
return j;
}
int main(){
int n;
cin>>n;
cout<<(jc(2*n)/(jc(n)*jc(n)))/(n+1);
return 0;
}
做法三:卡特兰数递推公式O(n)
递推公式的含义就是通过前面算出来过的东西来推出已知的
那么根据我的第六感
肯定是和
C
n
−
1
有关系
具有什么关系呢
盲猜一手,乘法关系
所以我们只需要把
C
n
和
C
n
−
1
=
相除就可以啦
C
n
/
C
n
−
1
=
C
2
n
n
n
+
1
/
C
2
n
−
2
n
−
1
n
=
C
2
n
n
n
+
1
×
n
C
2
n
−
2
n
−
1
=
(
2
n
)
!
n
!
n
!
n
+
1
×
n
(
2
n
−
2
)
!
(
n
−
1
)
!
(
n
−
1
)
!
=
(
2
n
)
!
n
!
n
!
n
+
1
×
n
(
n
−
1
)
!
(
n
−
1
)
!
(
2
n
−
2
)
!
=
(
2
n
)
!
n
!
n
!
n
(
n
−
1
)
!
(
n
−
1
)
!
(
n
+
1
)
(
2
n
−
2
)
!
=
(
2
n
)
!
n
!
(
n
−
1
)
!
(
n
−
1
)
!
(
n
−
1
)
!
(
n
+
1
)
(
2
n
−
2
)
!
=
(
2
n
)
!
n
!
(
n
−
1
)
!
(
n
+
1
)
(
2
n
−
2
)
!
=
(
2
n
)
!
(
n
−
1
)
!
n
(
n
−
1
)
!
(
n
+
1
)
(
2
n
−
2
)
!
=
(
2
n
)
!
n
(
n
+
1
)
(
2
n
−
2
)
!
=
(
2
n
)
!
n
(
n
+
1
)
(
2
n
−
2
)
!
=
(
2
n
)
!
(
2
n
)
(
2
n
−
1
)
n
(
n
+
1
)
(
2
n
)
!
=
(
2
n
)
(
2
n
−
1
)
n
(
n
+
1
)
=
4
×
n
2
−
2
n
n
2
+
n
上下两边同时除以
n
=
4
×
n
−
2
n
+
1
所以说
C
n
=
C
n
−
1
×
4
×
n
−
2
n
+
1
把这个公式带到代码里面去就行了
递推公式的含义就是通过前面算出来过的东西来推出已知的\\ 那么根据我的第六感\\ 肯定是和C_{n-1}有关系\\ 具有什么关系呢\\ 盲猜一手,乘法关系\\ 所以我们只需要把C_n和C_{n-1}\\ =相除就可以啦\\ C_n/C_{n-1}\\ =\frac{C^n_{2n}}{n+1}/\frac{C^{n-1}_{2n-2}}{n}\\ =\frac{C^n_{2n}}{n+1}\times\frac{n}{C^{n-1}_{2n-2}}\\ =\frac{\frac{(2n)!}{n!n!}}{n+1}\times\frac{n}{\frac{(2n-2)!}{(n-1)!(n-1)!}}\\ =\frac{\frac{(2n)!}{n!n!}}{n+1}\times\frac{n(n-1)!(n-1)!}{(2n-2)!}\\ =\frac{\frac{(2n)!}{n!n!}n(n-1)!(n-1)!}{(n+1)(2n-2)!}\\ =\frac{\frac{(2n)!}{n!(n-1)!}(n-1)!(n-1)!}{(n+1)(2n-2)!}\\ =\frac{\frac{(2n)!}{n!}(n-1)!}{(n+1)(2n-2)!}\\ =\frac{\frac{(2n)!}{(n-1)!n}(n-1)!}{(n+1)(2n-2)!}\\ =\frac{\frac{(2n)!}{n}}{(n+1)(2n-2)!}\\ =\frac{(2n)!}{n(n+1)(2n-2)!}\\ =\frac{(2n)!(2n)(2n-1)}{n(n+1)(2n)!}\\ =\frac{(2n)(2n-1)}{n(n+1)}\\ =\frac{4\times n^2-2n}{n^2+n}\\ 上下两边同时除以n\\ =\frac{4\times n-2}{n+1}\\ 所以说\\ C_n=C_{n-1}\times\frac{4\times n-2}{n+1} 把这个公式带到代码里面去就行了
递推公式的含义就是通过前面算出来过的东西来推出已知的那么根据我的第六感肯定是和Cn−1有关系具有什么关系呢盲猜一手,乘法关系所以我们只需要把Cn和Cn−1=相除就可以啦Cn/Cn−1=n+1C2nn/nC2n−2n−1=n+1C2nn×C2n−2n−1n=n+1n!n!(2n)!×(n−1)!(n−1)!(2n−2)!n=n+1n!n!(2n)!×(2n−2)!n(n−1)!(n−1)!=(n+1)(2n−2)!n!n!(2n)!n(n−1)!(n−1)!=(n+1)(2n−2)!n!(n−1)!(2n)!(n−1)!(n−1)!=(n+1)(2n−2)!n!(2n)!(n−1)!=(n+1)(2n−2)!(n−1)!n(2n)!(n−1)!=(n+1)(2n−2)!n(2n)!=n(n+1)(2n−2)!(2n)!=n(n+1)(2n)!(2n)!(2n)(2n−1)=n(n+1)(2n)(2n−1)=n2+n4×n2−2n上下两边同时除以n=n+14×n−2所以说Cn=Cn−1×n+14×n−2把这个公式带到代码里面去就行了
代码:
//这种方法在Acwing上回爆long long,请开高精度
#include<iostream>
using namespace std;
const int N=20;
long long C[N];
int main(){
int n;
cin>>n;
C[0]=1;
for(int i=1;i<=n;i++){
C[i]=((4*i-2)*1.0/(i+1))*C[i-1];
}
cout<<C[n];
return 0;
}
现在我们就把这一道题的所有解法写完了
现在我们来看另一道题目
括号匹配序列
这道题在哪个地方都没有原题
所以我们自己写一个
你可以创造一个长度为2n的括号序列,这个序列只包含’(‘或者’)‘,并且这个序列是合法的(没一个括号都能匹配的上),请问构造这种序列的总方案
这道题很简单,其实和刚刚的进出栈序列是一样的
把’(‘看成+1,把’)'看成-1,每一个+1后面都必须要有一个-1
这不就是纯卡特兰属吗?
所以这道题直接套用小公式就可以过了
二叉树
有人可能会问,卡特兰数和二叉树有什么关系呢
有几种二叉树都和卡特兰数有关呢
1.(国际)满二叉树
(国际)满二叉树的定义就是
对于一个节点
他要么是个叶子节点,要么他有两个儿子
那么我们把左节点叫做+1,把右节点叫做-1
随后进行先序遍历
得到先序遍历序列
你会发现还是卡特兰数
因为每个+1后面都会有一个-1
2.概率论(紫题)
luogu原题
这道题如果你的思维比较好
,
那么你应该还是知道怎么做的
首先我们要知道叶子节点数的期望是什么意思
n
个节点的叶子节点数的期望
=
n
个节点能构成的树的总方案
/
所有的叶子节点个数
首先我们来求
n
个节点能构成的叶子节点数的总方案
假设
n
个节点能构成的叶子节点数的总方案为
f
n
那么我们可以枚举他的左子树节点个数
然而左子树的形状我们是不知道的
所以说我们设左子树的节点个数为
i
那么左子树的总方案为
f
i
右子树的总方案就为
f
n
−
i
−
1
为什么呢
?
因为总结点数为
n
,
左子树节点数为
i
,
根节点为
1
,
相减即可
你会发现这两个数具有乘法原理
然后
i
的范围是
0
到
n
−
1
因为左子树个数可以为
0
,
所以
f
0
=
1
所以就可以得出公式
f
n
=
∑
i
=
0
n
−
1
f
i
f
n
−
i
−
1
你就会发现这是卡特兰数
O
(
n
2
)
的递推公式
我们就可以把它换一下
换成
O
(
n
)
递推公式
(
别问我为什么不写通项公式,因为待会用递推公式更好用
)
f
n
=
4
∗
n
−
2
n
+
1
f
n
−
1
然后我们去推总子节点个数
我们设
d
i
为
n
个节点的总子节点个数我们可以得到一些规律
d
i
=
i
×
f
i
−
1
这个我不知道为什么,打表打出来的
知道的人可以在下面评论
然后那么答案就是
f
n
d
n
套入公式
d
n
f
n
=
n
×
f
n
−
1
4
∗
n
−
2
n
+
1
f
n
−
1
=
n
4
∗
n
−
2
n
+
1
=
n
(
n
+
1
)
4
∗
n
−
2
好啦,这样提交上去就可以了
(
记得开
d
o
u
b
l
e
)
代码在这里就不放了,自己去写
这道题如果你的思维比较好,那么你应该还是知道怎么做的\\ 首先我们要知道叶子节点数的期望是什么意思\\ n个节点的叶子节点数的期望=n个节点能构成的树的总方案/所有的叶子节点个数\\ 首先我们来求n个节点能构成的叶子节点数的总方案\\ 假设n个节点能构成的叶子节点数的总方案为f_n\\ 那么我们可以枚举他的左子树节点个数\\ 然而左子树的形状我们是不知道的\\ 所以说我们设左子树的节点个数为i\\ 那么左子树的总方案为f_i\\ 右子树的总方案就为f_{n-i-1}\\ 为什么呢?\\ 因为总结点数为n,左子树节点数为i,根节点为1,相减即可\\ 你会发现这两个数具有乘法原理\\ 然后i的范围是0到n-1\\ 因为左子树个数可以为0,所以f_0=1\\ 所以就可以得出公式\\ f_n=\sum_{i=0}^{n-1}f_if_{n-i-1}\\ 你就会发现这是卡特兰数O(n^2)的递推公式\\ 我们就可以把它换一下\\ 换成O(n)递推公式(别问我为什么不写通项公式,因为待会用递推公式更好用)\\ f_n=\frac{4*n-2}{n+1}f_{n-1}\\ 然后我们去推总子节点个数\\ 我们设d_i为n个节点的总子节点个数 我们可以得到一些规律\\ d_i=i\times f_{i-1}\\ 这个我不知道为什么,打表打出来的\\ 知道的人可以在下面评论\\ 然后那么答案就是\frac{f_n}{d_n}\\ 套入公式\\ \frac{d_n}{f_n}\\ =\frac{n\times f_{n-1}}{\frac{4* n-2}{n+1}f_{n-1}}\\ =\frac{n}{\frac{4* n-2}{n+1}}\\ =\frac{n(n+1)}{4* n-2}\\ 好啦,这样提交上去就可以了(记得开double)\\ 代码在这里就不放了,自己去写
这道题如果你的思维比较好,那么你应该还是知道怎么做的首先我们要知道叶子节点数的期望是什么意思n个节点的叶子节点数的期望=n个节点能构成的树的总方案/所有的叶子节点个数首先我们来求n个节点能构成的叶子节点数的总方案假设n个节点能构成的叶子节点数的总方案为fn那么我们可以枚举他的左子树节点个数然而左子树的形状我们是不知道的所以说我们设左子树的节点个数为i那么左子树的总方案为fi右子树的总方案就为fn−i−1为什么呢?因为总结点数为n,左子树节点数为i,根节点为1,相减即可你会发现这两个数具有乘法原理然后i的范围是0到n−1因为左子树个数可以为0,所以f0=1所以就可以得出公式fn=i=0∑n−1fifn−i−1你就会发现这是卡特兰数O(n2)的递推公式我们就可以把它换一下换成O(n)递推公式(别问我为什么不写通项公式,因为待会用递推公式更好用)fn=n+14∗n−2fn−1然后我们去推总子节点个数我们设di为n个节点的总子节点个数我们可以得到一些规律di=i×fi−1这个我不知道为什么,打表打出来的知道的人可以在下面评论然后那么答案就是dnfn套入公式fndn=n+14∗n−2fn−1n×fn−1=n+14∗n−2n=4∗n−2n(n+1)好啦,这样提交上去就可以了(记得开double)代码在这里就不放了,自己去写
本期就这样完结了,这篇浅析写了作者5,6天的样子,希望能给个好的评价