CC++ & Algorithm

卡特兰数

困难3
语言版本:C++
概述:卡特兰数就像排队时“红色积木不能比蓝色积木少”的规则,它告诉我们有多少种不同的排队方法。

卡特兰数:排队、括号、出栈背后的神奇数字

小朋友们,你们有没有看过爸爸和小朋友一起排队买冰淇淋?爸爸说:“我走在前面,你要跟在我后面,不能跑到我前头。”如果一共有n个爸爸和n个小朋友,他们排成一列,要求任何时候爸爸的人数都不少于小朋友的人数(否则小朋友就会跑到前面去啦),那么一共有多少种不同的排队方式呢?这个问题的答案就是“卡特兰数”。

卡特兰数是一个很神奇的数字序列。它有许多生活中的影子,比如:用n对括号组成正确的括号表达式(左括号不能少于右括号)、n个元素进栈后有多少种出栈顺序、n个节点能组成多少种不同的二叉树……

生活中的卡特兰数——从排队到括号

先让我们把“爸爸和小朋友排队”的规则再讲清楚:
假设有n个爸爸(用“红”色积木表示)和n个小朋友(用“蓝”色积木表示),排成一列。从队伍最前面往后看,任何时候已经数过的红色积木数量都不能少于蓝色积木数量。如果蓝色比红色多,就说明小朋友跑到爸爸前面去了,这种排队方式不合格。

这样的排队方式有多少种?n=1时,只有“红蓝”一种(因为“蓝红”一开始蓝色就多了一个,不合格)。n=2时,有“红红蓝蓝”和“红蓝红蓝”两种。n=3时就有5种了。这些数字1,2,5...就是卡特兰数。

除了排队,卡特兰数还出现在这些地方:

  • 括号匹配:n对括号,能组成多少种合法的括号序列?比如n=2时,合法的有 ()()(()) 两种。左括号就像“爸爸”,右括号就像“小朋友”。
  • 栈的出栈顺序:n个不同的元素按顺序入栈,有多少种不同的出栈顺序?比如元素1,2,3入栈,出栈顺序有5种(如1,2,3;1,3,2;2,1,3;2,3,1;3,2,1),正对应n=3的卡特兰数C(3)=5。
  • 二叉树形态:n个节点能组成多少种不同的二叉树(左子树和右子树交换算不同)?比如n=3时,有5种形态。

这些看似不同的问题,答案都是同一个数字序列,是不是很神奇?

卡特兰数的计算公式

怎么计算卡特兰数呢?有一个简单的递推公式:

C(0) = 1
C(1) = 1
C(n) = C(0)×C(n-1) + C(1)×C(n-2) + ... + C(n-1)×C(0)

例如:
C(2) = C(0)×C(1) + C(1)×C(0) = 1×1 + 1×1 = 2
C(3) = C(0)×C(2) + C(1)×C(1) + C(2)×C(0) = 1×2 + 1×1 + 2×1 = 5

这个公式怎么理解?你可以想象把排队分成两部分:第一个“爸爸”出现,然后后面跟着一堆小朋友和爸爸的组合。更准确地说,对于n对(爸爸和小朋友),我们可以用“第一个爸爸和他匹配的小朋友”来分割队列,这样左右两边就变成了更小的子问题。

还有一个更快的组合数公式(可以查资料了解):
C(n) = (1/(n+1)) * C(2n, n)
但用递推公式写程序更直观。

用C++计算卡特兰数——一步步来

下面我们用C++写一个小程序,输入n,输出前n个卡特兰数。注意,数字可能变大,我们用long long类型存储。

#include <iostream>
using namespace std;

int main() {
    int n;  // 输入的个数
    cout << "请输入一个数字n: ";
    cin >> n;
    
    // 注意:卡特兰数增长很快,n最好不要超过20,否则long long会溢出
    long long catalan[25] = {0};  // 用数组存储卡特兰数,最大下标24
    catalan[0] = 1;  // C(0) = 1
    
    // 递推计算:先算C(1),再算C(2),...
    for (int i = 1; i <= n; i++) {
        catalan[i] = 0;  // 初始化为0,方便累加
        // 利用递推公式:C(i) = sum_{j=0}^{i-1} C(j) * C(i-1-j)
        for (int j = 0; j < i; j++) {
            catalan[i] += catalan[j] * catalan[i - 1 - j];
        }
    }
    
    cout << "前" << n << "个卡特兰数是:" << endl;
    for (int i = 0; i <= n; i++) {
        cout << "C(" << i << ") = " << catalan[i] << endl;
    }
    
    return 0;
}

试着运行一下,输入n=5,你会得到:
C(0)=1, C(1)=1, C(2)=2, C(3)=5, C(4)=14, C(5)=42。

如果你输入n=10,能得到C(10)=16796。但如果你输入n=20,小心结果可能超过long long的范围(最大约9×10^18,C(20)大约是6.56×10^9,还好;但C(35)就超过2^63了)。

常见错误与注意事项

新手在写卡特兰数程序时,容易犯这些错误:

  1. 数组下标搞混:递推内层循环 j 从0到i-1,对应 catalan[j] * catalan[i-1-j]。有人会写成 catalan[j] * catalan[i-j],导致结果错误。

  2. 忘记初始化catalan[0] 必须设为1。如果忘记,所有结果会变成0。

  3. 整数溢出:卡特兰数增长极快。n=30时,C(30) ≈ 3.8×10^16,还在long long范围内(9.2×10^18),但n=35就超了。如果需要计算更大的n,要使用高精度(大数)类型,比如用字符串或使用Python。

  4. 数组大小不足:如果用户输入n=30,而数组只开了25,就会越界访问。应该根据输入动态分配或者设定足够大的固定大小。为了安全,可以让用户输入n<=20,或者使用vector<long long>

  5. 递推顺序错误:一定要从小的i往大的i算,因为计算C(i)需要用到所有C(j) (j<i)。如果先算大的,小的还没算出来,结果会是0。

完整可运行示例(改进版)

下面是一个更健壮的版本,加入了输入范围检查和动态数组(用C++ vector),变量名都加中文注释:

#include <iostream>
#include <vector>   // 使用动态数组,可以根据输入大小灵活分配
using namespace std;

int main() {
    int n;  // 要计算的卡特兰数下标
    cout << "请输入一个数字n (n <= 30): ";
    cin >> n;

    // 检查输入范围,避免溢出
    if (n < 0 || n > 30) {
        cout << "n应该在0到30之间,否则结果可能溢出。" << endl;
        return 1;  // 返回错误码
    }

    // 定义动态数组,长度为n+1,全部初始化为0
    vector<long long> catalan(n + 1, 0);
    catalan[0] = 1;  // C(0) = 1

    // 递推计算卡特兰数
    for (int i = 1; i <= n; i++) {
        catalan[i] = 0;  // 显式清零(vector已经初始化为0,这一步可省略)
        for (int j = 0; j < i; j++) {
            // C(i) = sum_{j=0}^{i-1} C(j) * C(i-1-j)
            catalan[i] += catalan[j] * catalan[i - 1 - j];
        }
    }

    // 输出结果
    cout << "前" << n << "个卡特兰数是:" << endl;
    for (int i = 0; i <= n; i++) {
        cout << "C(" << i << ") = " << catalan[i] << endl;
    }

    return 0;
}

运行示例(输入6):

请输入一个数字n (n <= 30): 6
前6个卡特兰数是:
C(0) = 1
C(1) = 1
C(2) = 2
C(3) = 5
C(4) = 14
C(5) = 42
C(6) = 132

想一想:如果爸爸和小朋友一共有3个爸爸和3个小朋友,按照规则排队,是不是恰好有5种排法呢?你可以用红蓝积木实际摆一摆,验证卡特兰数的神奇哦!

相关知识点指引

  • 组合数学:卡特兰数是组合数学中重要的计数序列,可以学习用组合公式计算(即C(n) = C(2n,n)/(n+1)),以及如何用组合意义证明。
  • 动态规划:卡特兰数的递推公式就是典型的动态规划思想——把大问题分解成小问题,用状态转移方程求解。这对学习DP很有帮助。
  • 栈和二叉树:出栈顺序、二叉树计数等问题的解法都依赖卡特兰数,可以进一步学习栈的应用和二叉树遍历。
  • 高精度计算:当n较大时,需要实现大整数乘法(比如用字符串或数组模拟),这也是CSP-S中常见的高精度题目。

如果你对卡特兰数的证明感兴趣,可以查一查“折线法”或“反射原理”,它们非常巧妙地解释了为什么递推公式成立。

例题精讲

1单选题

卡特兰数 h(n) 的递推公式(n≥1,h(0)=1)是下列哪一个?

Ah(n)=∑_{i=0}^{n-1} h(i)h(n-1-i)
Bh(n)=∑_{i=0}^{n} h(i)h(n-i)
Ch(n)=h(n-1)+h(n-2)
Dh(n)=n*h(n-1)
2判断题

已知卡特兰数 h(n)=C(2n,n)/(n+1),则 h(3)=5。

3填空题
计算第 n 个卡特兰数(n≤30)的动态规划代码,补全循环部分。
long long catalan[31];
catalan[0]=1;
for(int i=1;i<=n;i++){
    ___
}
4单选题

n个结点的不同形态的二叉树数目(不考虑结点值)是卡特兰数 h(n)。已知 h(0)=1,那么 n=4 时有多少种?

A8
B12
C14
D16
5判断题

对于 n 对括号,合法的括号序列个数等于卡特兰数 h(n)。例如 n=2 时,合法序列有 "()()" 和 "(())" 两种,而 h(2)=2,所以说法正确。