【算法基础篇】(五十五)卡特兰数封神之路:从括号匹配到二叉树构造,组合数学的万能钥匙!


目录

​编辑

前言

[一、卡特兰数的定义:什么是 "神奇数列"?](#一、卡特兰数的定义:什么是 “神奇数列”?)

[1.1 核心定义](#1.1 核心定义)

[1.2 卡特兰数的本质](#1.2 卡特兰数的本质)

[二、卡特兰数的 4 大核心公式:从递推到通项](#二、卡特兰数的 4 大核心公式:从递推到通项)

[2.1 公式一:递推公式(分治思想)](#2.1 公式一:递推公式(分治思想))

[2.2 公式二:通项公式(组合数直接计算)](#2.2 公式二:通项公式(组合数直接计算))

[2.3 公式三:递推优化公式(线性递推)](#2.3 公式三:递推优化公式(线性递推))

[2.4 公式四:组合数差公式(正难则反)](#2.4 公式四:组合数差公式(正难则反))

[2.5 公式对比与选型建议](#2.5 公式对比与选型建议)

三、卡特兰数的经典应用:从理论到实践

[3.1 括号匹配问题](#3.1 括号匹配问题)

[3.2 栈的出栈序列问题](#3.2 栈的出栈序列问题)

[3.3 购票找零问题](#3.3 购票找零问题)

[3.4 二叉树构造问题](#3.4 二叉树构造问题)

[3.5 圆上点配对问题](#3.5 圆上点配对问题)

[3.6 阶梯搭建问题](#3.6 阶梯搭建问题)

[四、卡特兰数的 C++ 实现:5 道例题从入门到高阶](#四、卡特兰数的 C++ 实现:5 道例题从入门到高阶)

[4.1 例题 1:矩阵 II(洛谷 P1722)------ 模数非质数,用递推公式](#4.1 例题 1:矩阵 II(洛谷 P1722)—— 模数非质数,用递推公式)

[4.2 例题 2:栈(洛谷 P1044)------ 无取模,用线性递推公式](#4.2 例题 2:栈(洛谷 P1044)—— 无取模,用线性递推公式)

[4.3 例题 3:球迷购票问题(洛谷 P1754)------ 无取模,用线性递推公式](#4.3 例题 3:球迷购票问题(洛谷 P1754)—— 无取模,用线性递推公式)

[4.4 例题 4:小猫配对(洛谷 P1375)------ 模数为质数,用通项公式](#4.4 例题 4:小猫配对(洛谷 P1375)—— 模数为质数,用通项公式)

[4.5 例题 5:树屋阶梯(洛谷 P2532)------ 无取模,用高精度递推公式](#4.5 例题 5:树屋阶梯(洛谷 P2532)—— 无取模,用高精度递推公式)

五、卡特兰数的常见误区与避坑指南

[5.1 溢出问题](#5.1 溢出问题)

[5.2 公式选型错误](#5.2 公式选型错误)

[5.3 问题本质识别错误](#5.3 问题本质识别错误)

[5.4 高精度计算细节](#5.4 高精度计算细节)

总结


前言

你有没有想过:n 对括号有多少种合法匹配方式?1,2,3...n 依次入栈,有多少种合法出栈序列?圆上 2n 个点两两相连,怎样保证线段不交叉?这些看似毫无关联的问题,背后都指向同一个神奇的数列 ------卡特兰数

**卡特兰数(Catalan Number)**是组合数学中的 "明星数列",以比利时数学家欧仁・夏尔・卡塔兰命名。它就像一把万能钥匙,能解锁无数计数难题,不仅是算法面试的 "高频考点"(字节、阿里、腾讯等大厂笔试常客),还广泛应用于括号匹配、栈操作、二叉树构造、路径规划等场景。

本文将带你揭开卡特兰数的神秘面纱:从生活场景切入,推导核心公式,拆解经典应用,再通过 5 道梯度例题(从入门到高阶),手把手教你用 C++ 实现高效解法。无论你是算法新手,还是想巩固组合数学的开发者,读完这篇文章,都能彻底掌握卡特兰数的核心逻辑,轻松应对各类相关问题!


一、卡特兰数的定义:什么是 "神奇数列"?

1.1 核心定义

卡特兰数是一组满足特定递推关系的整数序列,记为Cn​(n≥0),其中第 n 项表示 n 个 "操作对" 对应的合法方案数。

前 11 项卡特兰数(n 从 0 到 10)如下:

n(项数) 0 1 2 3 4 5 6 7 8 9 10
Cn​(卡特兰数) 1 1 2 5 14 42 132 429 1430 4862 16796

从数列可以看出,卡特兰数增长速度极快,n=10 时已达 16796,n=20 时更是突破 6564120420,因此大规模计算时需注意溢出问题(通常用取模或高精度处理)。

1.2 卡特兰数的本质

卡特兰数的核心本质是:在两种对立操作的序列中,保证任意前缀中一种操作数不小于另一种操作数

比如:

  • 括号匹配:左括号(+1)和右括号(-1),任意前缀和≥0;
  • 栈操作:入栈(+1)和出栈(-1),任意前缀和≥0;
  • 购票找零:50 元(+1)和 100 元(-1),任意前缀和≥0。

这些问题的共性,正是卡特兰数的应用场景标志。

二、卡特兰数的 4 大核心公式:从递推到通项

卡特兰数有 4 种常用公式,分别适用于不同场景(如数据范围、是否取模、是否需要递推打表),掌握它们就能应对所有卡特兰数问题。

2.1 公式一:递推公式(分治思想)

推导逻辑 :将规模为 n 的问题拆分为两个子问题。以**"n 个结点构造二叉树"** 为例:

  • 选择第 i 个结点作为根节点;
  • 左子树有 i 个结点,方案数为
  • 右子树有 n-1-i 个结点,方案数为
  • 总方案数为所有拆分方式的累加,即上述递推公式。

适用场景:n 较小(≤100)、需要规避除法(如取模的模数不是质数)。

时间复杂度O(n^2)

2.2 公式二:通项公式(组合数直接计算)

推导逻辑:从 2n 个位置中选择 n 个位置放置 "左操作"(如左括号),总方案数为(n2n​);再减去非法方案数(任意前缀中 "右操作" 多于 "左操作"),最终得到上述通项公式(推导过程见下文 "经典问题")。

适用场景:n 中等(≤1e5)、模数为质数(可通过逆元处理除法)。

时间复杂度O(n)(预处理阶乘和逆元后,单次查询O(1))。

2.3 公式三:递推优化公式(线性递推)

推导逻辑:由通项公式变形得到,避免了组合数计算,直接通过前一项递推当前项。

适用场景:n 中等(≤1e6)、无需取模(或模数支持除法)、需要快速打表。

时间复杂度O(n)(线性递推,效率极高)。

2.4 公式四:组合数差公式(正难则反)

推导逻辑:总方案数(n2n​)减去非法方案数(n−12n​)(非法方案数的证明见下文 "经典问题")。

适用场景:n 较小(≤20)、无需取模(直接计算组合数)。

时间复杂度O(n)(计算两个组合数)。

2.5 公式对比与选型建议

公式类型 优点 缺点 适用场景
递推公式(公式一) 无除法、逻辑直观 时间复杂度O(n2) n≤100、模数非质数
通项公式(公式二) 组合数本质、支持大 n 需要处理除法(逆元) n≤1e5、模数为质数
线性递推(公式三) 效率最高(O(n))、代码简洁 除法可能溢出 n≤1e6、无需取模或模数支持除法
组合数差(公式四) 逻辑简单(正难则反) 组合数计算可能溢出 n≤20、直接计算(无取模)

实际应用中,优先根据数据范围取模要求选择公式:

  • 小 n(≤20):公式四(直接计算);
  • 中 n(≤1e5):公式二(阶乘 + 逆元);
  • 大 n(≤1e6):公式三(线性递推);
  • 模数非质数:公式一(递推规避除法)。

三、卡特兰数的经典应用:从理论到实践

卡特兰数的应用场景看似零散,实则都符合 "任意前缀操作数约束" 的核心逻辑。以下是 6 类经典应用,帮你快速识别卡特兰数问题:

3.1 括号匹配问题

问题:n 对括号,有多少种合法匹配方式?

示例 :n=2 时,合法方式为(())()(),共 2 种(对应C2​=2)。

核心逻辑:左括号数≥右括号数(任意前缀)。

3.2 栈的出栈序列问题

问题:1,2,...,n 依次入栈,有多少种合法出栈序列?

示例:n=3 时,合法序列为 123、132、213、231、321,共 5 种(对应C3​=5)。

核心逻辑:入栈次数≥出栈次数(任意前缀)。

3.3 购票找零问题

问题:n 人持 50 元、n 人持 100 元购票(票价 50 元),售票员无初始零钱,有多少种排队方式能顺利找零?

示例:n=2 时,合法排队方式为 50,50,100,10050,100,50,100,共 2 种(对应C2​=2)。

核心逻辑:50 元人数≥100 元人数(任意前缀)。

3.4 二叉树构造问题

问题:n 个结点,能构造多少种不同的二叉搜索树(或普通二叉树)?

示例:n=3 时,共 5 种(对应C3​=5)。

核心逻辑:根节点左子树有 i 个结点,右子树有 n-1-i 个结点,所有拆分方式累加。

3.5 圆上点配对问题

问题:圆上有 2n 个点,两两相连且线段不交叉,有多少种配对方式?

示例:n=3 时,共 5 种(对应C3​=5)。

核心逻辑:固定一个点,与另一个点配对,将圆分成两部分,两部分的点各自配对不交叉。

3.6 阶梯搭建问题

问题:用 n 个 1×2 的矩形覆盖 2×n 的矩形,或用 n 个钢材搭建高度为 n 的阶梯(每步宽高均为 1),有多少种方式?

示例:n=3 时,共 5 种(对应C3​=5)。

核心逻辑:每一步选择 "横向延伸" 或 "纵向延伸",保证阶梯合法。

四、卡特兰数的 C++ 实现:5 道例题从入门到高阶

4.1 例题 1:矩阵 II(洛谷 P1722)------ 模数非质数,用递推公式

题目链接:https://www.luogu.com.cn/problem/P1722

题目描述:给定 n,构造 1×2n 的矩阵,放入红色(+1)和黑色(-1)算筹,要求任意前缀红色数≥黑色数,且红黑数相等,求方案数对 100 取模的结果(n≤100)。

输入示例:2 → 输出示例:2

解题思路

  • 问题本质是 n 对括号的合法匹配,即卡特兰数Cn;
  • 模数 100 不是质数,无法用逆元处理除法,选择公式一(递推公式);
  • 递推公式:,对 100 取模。

C++ 代码实现

cpp 复制代码
#include <iostream>
using namespace std;

const int N = 110;
const int MOD = 100; // 非质数,规避除法

int main() {
    int n;
    cin >> n;
    int catalan[N] = {0};
    catalan[0] = 1; // 初始项C0=1
    
    // 公式一:递推计算C1到Cn
    for (int i = 1; i <= n; ++i) {
        for (int j = 0; j < i; ++j) {
            catalan[i] = (catalan[i] + catalan[j] * catalan[i - j - 1]) % MOD;
        }
    }
    
    cout << catalan[n] << endl;
    return 0;
}

代码解析

  • 用数组catalan存储卡特兰数,初始项catalan[0]=1
  • 双重循环实现递推,外层循环计算第 i 项,内层循环累加所有子问题的乘积;
  • 每步取模 100,避免溢出,同时满足题目要求。

4.2 例题 2:栈(洛谷 P1044)------ 无取模,用线性递推公式

题目链接:https://www.luogu.com.cn/problem/P1044

题目描述:给定 n,计算 1,2,...,n 依次入栈的合法出栈序列数(n≤18)。

输入示例:3 → 输出示例:5

解题思路

  • 问题本质是栈的合法出栈序列,即卡特兰数Cn;
  • n≤18,卡特兰数=477638700,未超过long long范围(最大值 9e18);
  • 选择公式三(线性递推),效率最高,代码简洁。

C++ 代码实现

cpp 复制代码
#include <iostream>
using namespace std;

typedef long long LL; // 存储较大的卡特兰数

int main() {
    int n;
    cin >> n;
    LL catalan = 1; // C0=1
    
    // 公式三:线性递推 Cn = Cn-1 * (4n-2)/(n+1)
    for (int i = 1; i <= n; ++i) {
        catalan = catalan * (4 * i - 2) / (i + 1);
    }
    
    cout << catalan << endl;
    return 0;
}

代码解析

  • long long存储结果,避免 n=18 时溢出;
  • 线性递推公式无需双重循环,时间复杂度O(n);
  • 由于卡特兰数是整数,(4i-2) 必能被 (i+1) 整除,无需担心除法精度问题。

4.3 例题 3:球迷购票问题(洛谷 P1754)------ 无取模,用线性递推公式

题目链接:https://www.luogu.com.cn/problem/P1754

题目描述:n 人持 50 元、n 人持 100 元购票(票价 50 元),售票员无初始零钱,求合法排队方式数(n≤20)。

输入示例:2 → 输出示例:2

解题思路

  • 问题本质是购票找零,即卡特兰数Cn;
  • n≤20,C20=6564120420,仍在long long范围;
  • 选择公式三(线性递推),代码复用性高。

C++ 代码实现

cpp 复制代码
#include <iostream>
using namespace std;

typedef long long LL;

int main() {
    int n;
    cin >> n;
    LL catalan = 1;
    
    for (int i = 1; i <= n; ++i) {
        catalan = catalan * (4 * i - 2) / (i + 1);
    }
    
    cout << catalan << endl;
    return 0;
}

代码解析

  • 与 "栈的出栈序列" 代码完全一致,体现了卡特兰数问题的共性;
  • 无需修改核心逻辑,仅需理解问题本质是卡特兰数应用。

4.4 例题 4:小猫配对(洛谷 P1375)------ 模数为质数,用通项公式

题目链接https://www.luogu.com.cn/problem/P1375

题目描述:2n 只小猫站成一圈,两两配对且绳子不交叉,求方案数对 1e9+7 取模的结果(n≤1e5)。

输入示例:3 → 输出示例:5

解题思路

  • 问题本质是圆上点配对,即卡特兰数Cn;
  • n≤1e5,需高效计算,选择公式二(通项公式);
  • 模数 1e9+7 是质数,用费马小定理求逆元,处理除法n+11。

C++ 代码实现

cpp 复制代码
#include <iostream>
using namespace std;

typedef long long LL;
const int MOD = 1e9 + 7;
const int N = 2e5 + 10; // 2n最大为2e5

LL fact[N];   // fact[i] = i! mod MOD
LL inv_fact[N]; // inv_fact[i] = (i!)^{-1} mod MOD

// 快速幂:计算a^b mod p(费马小定理求逆元)
LL qpow(LL a, LL b, LL p) {
    LL res = 1;
    while (b) {
        if (b & 1) res = res * a % p;
        a = a * a % p;
        b >>= 1;
    }
    return res;
}

// 预处理阶乘和阶乘逆元
void init(int max_n) {
    fact[0] = 1;
    for (int i = 1; i <= max_n; ++i) {
        fact[i] = fact[i - 1] * i % MOD;
    }
    // 费马小定理:inv_fact[max_n] = (max_n!)^(MOD-2) mod MOD
    inv_fact[max_n] = qpow(fact[max_n], MOD - 2, MOD);
    for (int i = max_n - 1; i >= 0; --i) {
        inv_fact[i] = inv_fact[i + 1] * (i + 1) % MOD;
    }
}

// 计算组合数C(n, k) mod MOD
LL comb(int n, int k) {
    if (n < 0 || k < 0 || n < k) return 0;
    return fact[n] * inv_fact[k] % MOD * inv_fact[n - k] % MOD;
}

// 计算卡特兰数Cn mod MOD(公式二:通项公式)
LL catalan(int n) {
    return comb(2 * n, n) * qpow(n + 1, MOD - 2, MOD) % MOD;
}

int main() {
    int n;
    cin >> n;
    init(2 * n); // 预处理到2n的阶乘
    cout << catalan(n) << endl;
    return 0;
}

代码解析

  • 预处理阶乘和逆元 :阶乘**fact[i]存储 i! mod MOD,逆元inv_fact[i]**存储 (i!)^{-1} mod MOD
  • 组合数计算comb(n, k) = n!/(k!*(n-k)!) mod MOD,通过阶乘和逆元实现;
  • 卡特兰数计算catalan(n) = C(2n, n) * inv(n+1) mod MOD,其中inv(n+1)是 n+1 的逆元(用快速幂计算);
  • 时间复杂度 :预处理O(n),查询O(1),适合 n≤1e5 的场景。

4.5 例题 5:树屋阶梯(洛谷 P2532)------ 无取模,用高精度递推公式

题目链接:https://www.luogu.com.cn/problem/P2532

题目描述:用 n 个钢材搭建高度为 n 的阶梯(每步宽高均为 1),求搭建方式数(n≤500)。

输入示例:3 → 输出示例:5

解题思路

  • 问题本质是阶梯搭建,即卡特兰数Cn;
  • n≤500,C500是极大数(远超long long范围),需用高精度计算;
  • 选择公式一(递推公式),规避除法,适合高精度实现。

C++ 代码实现

cpp 复制代码
#include <iostream>
using namespace std;

const int N = 510;   // 最大n=500
const int M = 1000;  // 高精度数组长度(足够存储C500)

// 高精度加法:a = a + b(a、b为逆序存储的大整数,低位在前)
void add(int a[], int b[]) {
    for (int i = 0; i < M - 1; ++i) {
        a[i] += b[i];
        a[i + 1] += a[i] / 10; // 进位
        a[i] %= 10;            // 取当前位
    }
}

int main() {
    int n;
    cin >> n;
    // catalan[i]存储C_i,逆序存储(catalan[i][0]是个位,catalan[i][1]是十位...)
    int catalan[N][M] = {0};
    catalan[0][0] = 1; // C0=1
    
    // 公式一:递推计算C1到Cn
    for (int i = 1; i <= n; ++i) {
        for (int j = 0; j < i; ++j) {
            add(catalan[i], catalan[j]);
            // 这里需要注意:原公式是C_i += C_j * C_{i-1-j},但上面的add是加法,怎么办?
            // 修正:上面代码有误,正确做法是用一个临时数组存储C_j * C_{i-1-j},再加到C_i中
            // 以下是修正后的核心逻辑:
            // 临时数组temp存储C_j * C_{i-1-j}
            int temp[M] = {0};
            for (int x = 0; x < M - 1; ++x) { // 遍历C_j的每一位
                if (catalan[j][x] == 0) continue;
                for (int y = 0; y < M - 1 - x; ++y) { // 遍历C_{i-1-j}的每一位
                    temp[x + y] += catalan[j][x] * catalan[i - 1 - j][y];
                    temp[x + y + 1] += temp[x + y] / 10; // 乘法进位
                    temp[x + y] %= 10;
                }
            }
            // 将temp加到catalan[i]中
            add(catalan[i], temp);
        }
    }
    
    // 输出结果:从高位到低位遍历,跳过前导零
    int p = M - 1;
    while (p >= 0 && catalan[n][p] == 0) --p;
    if (p < 0) cout << 0;
    else {
        while (p >= 0) cout << catalan[n][p--];
    }
    cout << endl;
    return 0;
}

代码解析

  • 高精度存储 :用二维数组**catalan[N][M]**存储卡特兰数,**catalan[i][j]**表示Ci的第 j 位(逆序存储,方便进位处理);
  • 高精度乘法 + 加法 :由于公式一是乘积之和,需先计算C_j * C_{i-1-j}(高精度乘法),再累加到C_i中(高精度加法);
  • 输出处理:从高位到低位遍历,跳过前导零,得到正确结果;
  • 时间复杂度:O(n2×M),n=500 时仍能高效运行。

五、卡特兰数的常见误区与避坑指南

5.1 溢出问题

  • 小 n(≤20):用long long存储;
  • 中 n(≤1e5):用取模 + 逆元(公式二);
  • 大 n(>1e5 或无取模):用高精度计算(公式一)。

5.2 公式选型错误

  • 模数非质数:避免用公式二、三(含除法),选择公式一;
  • 大 n 快速计算:优先用公式三(线性递推)或公式二(预处理阶乘);
  • 小 n 直接计算:用公式四(组合数差)更直观。

5.3 问题本质识别错误

  • 核心标志:两种操作、任意前缀一种操作数≥另一种;
  • 反例:若无 "任意前缀" 约束,可能不是卡特兰数问题(如 "n 对括号的所有排列数" 是(n2n),而非卡特兰数)。

5.4 高精度计算细节

  • 存储顺序:逆序存储(低位在前),方便进位处理;
  • 乘法进位:需双重循环遍历两个数的每一位,避免漏进位;
  • 前导零:输出时跳过高位零,否则会输出多余的 0。

总结

通过本文的学习,你不仅能掌握卡特兰数的解法,更能理解组合数学中 "抽象共性 + 具体实现" 的思维方式。下次遇到括号匹配、栈操作、二叉树构造等问题时,不妨先判断是否为卡特兰数应用,再套用对应的公式和代码模板,轻松解决问题!

如果本文对你有帮助,欢迎点赞、收藏、转发,也欢迎在评论区交流讨论~ 后续还会分享更多组合数学经典问题(如容斥原理、隔板法),关注我,一起玩转算法!

相关推荐
江畔柳前堤2 小时前
大语言模型分布式训练:从并行策略到万卡工程的系统梳理
人工智能·分布式·深度学习·算法·目标检测·机器学习·语言模型
Forever Nore3 小时前
LeetCode 4 寻找两个正序数组的中位数 - 二分
算法·leetcode
罗西的思考4 小时前
【OpenClaw具身硬件】MiniClaw 阅读笔记---(1)基础
人工智能·算法·机器学习
蛋先生DX5 小时前
大模型参数存储格式揭秘:BF不是男朋友
深度学习·算法·llm
爱跳舞的烤冷面6 小时前
自学嵌入式第22天(数据结构——哈希)
数据结构·算法·哈希算法
猎嘤一号6 小时前
博弈论(Game Theory)的理论、算法与工程
人工智能·算法·安全·博弈论
月光船幽幽6 小时前
影子模式下保护 logits 不被修改
人工智能·python·算法
马拉AI6 小时前
腾讯开源 Agent 记忆系统,AI“换对话就忘”的问题有了新解法(附安装使用教程)
人工智能·算法·开源·科研
地平线开发者8 小时前
征程6工具链模型X86推理方式说明
算法
地平线开发者8 小时前
【征程6】校准量化中HistogramObserver解析
算法