#简介
卡特兰数是组合数学中一个常出现在各种计数问题中的数列。以比利时的数学家欧仁·查理·卡塔兰 (1814–1894)的名字来命名。
数列的前几项为:1, 1, 2, 5, 14, 42, 132, 429, 1430, 4862,…
- 卡特兰数Cn满足以下递推关系:
- Cn+1 = C0Cn + C1Cn-1 + ··· + CnC0
##原理
- 设h(n)为catalan数的第n+1项,令h(0)=1,h(1)=1,catalan数满足递推式:
- h(n)= h(0) * h(n-1) + h(1) * h(n-2) + ··· + h(n-1) * h(0),(n>=2)
- 例如:
- h(2) = h(0) * h(1) + h(1) * h(0) = 11 + 11 = 2
- h(3) = h(0) * h(2) + h(1) * h(1) + h(2) * h(0) = 12+11+2*1 = 5
#应用
实质上都是递推等式的应用
关于卡特兰数应用实例有很多,有一本书就讲了将近200个“卡特兰数”问题,由于篇幅原因,这里不做过多赘述,有兴趣的同学可以深入研究
##进出栈问题
###问题描述
n个元素进栈序列为:1,2,3,4,…,n,则有多少种出栈序列。
思路:
-
我们将进栈表示为 +1,出栈表示为 -1,则 1 3 2 的出栈序列可以表示为:+1 -1 +1 +1 -1 -1。
-
根据栈本身的特点,每次出栈的时候,必定之前有元素入栈,即对于每个 -1 前面都有一个 +1 相对应。
-
因此,出栈序列的 所有前缀和 必然大于等于 0,并且 +1 的数量 等于 -1 的数量。
-
接下来非法序列怎么求?乍看之下完全没思路啊,如果用暴力解法那未免不要太麻烦,这时候我们采用逆向思维,就会发现这个问题并没有想象的那么难,公式如下:
合法序列数 = 总序列数 - 非法序列数 -
所以我们只需要求出总序列数和非法序列数然后相减即可得出合法序列数,总序列数很好求,即 C(n)(2n) ,那么非法序列数怎么求呢?
-
还是逆向思维,说实话直接求非法序列数同样比较难求,但是我们可以这样:
求得与非法序列数一一对应的另一种序列数,为了方便描述,这里称非法序列为 A,与 A 相对应的另一种序列为 B,对于B的定义是这样的:由于 A 是一个非法序列,必然存在某个位置的前缀和小于0的情况,那么,从左往右看,可以肯定的是,从某个位置开始,该位置前缀和突然小于0,且必然是-1(因为该位置是第一次前年缀和小于 0 的位置),比如序列:+1 -1 -1 +1 -1 +1,第三个1的前缀和首次小于0,那么我们就将该位置(包含该位置)之前的数字取反就得到了与A相对应的的序列 B:-1 +1 +1 +1 -1 +1,(怎么证明A和B是一一对应的关系呢?这么后面会讲,这里先记着肯定是一一对应的即可),这时候求得序列B的个数,即可得出 A 的个数,那么怎么求 B 的个数呢?
对于 B,由于其是将A的第一个前缀和小于零的位置(包含该位置)之前的数取反得来的,所以每个 B 都有 (n + 1) 个 +1 以及 (n - 1) 个 -1
,那么 B 的数量为 C(n+1)(2n) ,则 A 的数量同为 C(n+1)(2n)
则合法序列数 = 总序列数 - 非法序列数 = C(n)(2n) - C(n+1)(2n)
-
还有个问题,那么怎么证明A和B是一一对应的关系呢?
- 因为每个 A 只有一个"第一个前缀和小于 0 的前缀",所以每个 A 只能产生一个 B。而每个 B 想要还原到 A,就需要找到"第一个前缀和大于 0 的前缀",显然 B 也只能产生一个 A
-
因此同时满足以下条件即可转化为
卡特兰数问题
:- ① +1和-1的个数相同皆为n个(这里+1和-1是虚指,代表两种事物,且只有这两种)
- ② 序列每个位置的前缀和均大于等于0
最终,满足该条件的序列数为:C(n)(2n) - C(n+1)(2n),另一种形式则为:C(n)(2n)/(n+1),求和便是Catalan递归式
##代码实现
- Java实现
import java.math.BigInteger;
// 打印前 n 个卡特兰数
int n = 20;
BigInteger ans = BigInteger.valueOf(1);
System.out.println("1:" + ans.toString());
BigInteger four = BigInteger.valueOf(4);
BigInteger one = BigInteger.valueOf(1);
BigInteger two = BigInteger.valueOf(2);
for (int i = 2; i <= n; i++) {
BigInteger bi = BigInteger.valueOf(i);
ans = ans.multiply(four.multiply(bi).subtract(two)).divide(bi.add(one));
System.out.println(i + ":" + ans.toString());
}
- Python实现
# 打印前 n 个卡特兰数
ans, n = 1, 20
print("1:" + str(ans))
for i in range(2, n + 1):
ans = ans * (4 * i - 2) // (i + 1)
print(str(i) + ":" + str(ans))
##其他问题
1.二叉树
n + 1 个叶子节点能够构成多少种形状不同的(国际)满二叉树?
- (国际)满二叉树定义:如果一棵二叉树的结点要么是叶子结点,要么它有两个子结点,这样的树就是满二叉树。
- 思路
- 使用深度优先搜索这个满二叉树,向左扩展时标记为 +1,向右扩展时标记为 -1。
2. 括号问题
n 对括号,则有多少种 “括号匹配” 的括号序列?
- ( ( )( ) )···
- 这个完全可直接转化为出入栈问题
3. 排队问题
8 个高矮不同的人需要排成两队,每队 4 个人。其中,每排都是从低到高排列,且第二排的第 i 个人比第一排中第 i 个人高,则有多少种排队方式?
#总结
- 基本上所有的卡特兰数问题经过一定的转换都可以还原成进出栈问题。因此,只要我们能够学会进出栈问题的解法,无论问题再怎么变化,本质还是不变的。
- 卡特兰数问题中都会存在一种匹配关系,如进出栈匹配,括号匹配等,一旦计数问题中存在这种关系,那我们就需要去考虑这是否是卡特兰数问题。此外,我们还可以记住序列前四项:1, 1, 2, 5,这些将有利于我们联想到卡特兰数。