跳到主要内容
卡特兰数详解:从定义公式到经典应用与 C++ 实现 | 极客日志
C++ 算法
卡特兰数详解:从定义公式到经典应用与 C++ 实现 卡特兰数是组合数学中重要的整数序列,广泛应用于括号匹配、栈操作、二叉树计数等问题。本文详细解析了卡特兰数的四种核心公式,包括递推、通项及线性递推形式,并对比了不同场景下的选型策略。通过五道典型例题,涵盖模数非质数、无取模、高精度计算等场景,展示了 C++ 的具体实现方案。重点讲解了如何避免溢出、识别问题本质以及高精度运算的细节处理,帮助读者掌握从理论推导到代码落地的完整流程。
前言
你有没有想过:n 对括号有多少种合法匹配方式?1,2,3…n 依次入栈,有多少种合法出栈序列?圆上 2n 个点两两相连,怎样保证线段不交叉?这些看似毫无关联的问题,背后都指向同一个神奇的数列 ——卡特兰数 。
卡特兰数是组合数学中的核心概念之一,以比利时数学家欧仁・夏尔・卡塔兰命名。它就像一把万能钥匙,能解锁无数计数难题,不仅是算法面试的常见考点,还广泛应用于括号匹配、栈操作、二叉树构造等场景。
本文将带你揭开卡特兰数的面纱:从生活场景切入,推导核心公式,拆解经典应用,再通过 5 道梯度例题,手把手教你用 C++ 实现高效解法。无论你是算法新手,还是想巩固组合数学的开发者,读完这篇文章,都能彻底掌握卡特兰数的核心逻辑。
一、卡特兰数的定义:什么是'神奇数列'?
1.1 核心定义
卡特兰数是一组满足特定递推关系的整数序列,记为 $C_n$($n \ge 0$),其中第 n 项表示 n 个'操作对'对应的合法方案数。
前 11 项卡特兰数(n 从 0 到 10)如下:
n(项数) 0 1 2 3 4 5 6 7 8 9 10 $C_n$(卡特兰数) 1 1 2 5 14 42 132 429 1430 4862 16796
从数列可以看出,卡特兰数增长速度极快,n=10 时已达 16796,n=20 时更是突破 65 亿,因此大规模计算时需注意溢出问题(通常用取模或高精度处理)。
1.2 卡特兰数的本质
卡特兰数的核心本质是:在两种对立操作的序列中,保证任意前缀中一种操作数不小于另一种操作数 。
比如:
括号匹配 :左括号(+1)和右括号(-1),任意前缀和≥0;
栈操作 :入栈(+1)和出栈(-1),任意前缀和≥0;
购票找零 :50 元(+1)和 100 元(-1),任意前缀和≥0。
这些问题的共性,正是卡特兰数的应用场景标志。
二、卡特兰数的 4 大核心公式:从递推到通项
卡特兰数有 4 种常用公式,分别适用于不同场景(如数据范围、是否取模、是否需要递推打表),掌握它们就能应对所有卡特兰数问题。
2.1 公式一:递推公式(分治思想)
推导逻辑 :将规模为 n 的问题拆分为两个子问题。以'n 个结点构造二叉树'为例:选择第 i 个结点作为根节点;总方案数为所有拆分方式的累加。右子树有 n-1-i 个结点,左子树有 i 个结点,方案数相乘后累加。
适用场景 :n 较小(≤100)、需要规避除法(如取模的模数不是质数)。
时间复杂度 :O(n²) 。
2.2 公式二:通项公式(组合数直接计算)
推导逻辑 :从 2n 个位置中选择 n 个位置放置'左操作',总方案数为 $\binom{2n}{n}$;再减去非法方案数,最终得到上述通项公式。
适用场景 :n 中等(≤1e5)、模数为质数(可通过逆元处理除法)。
时间复杂度 :O(n) (预处理阶乘和逆元后,单次查询 O(1))。
2.3 公式三:递推优化公式(线性递推) 推导逻辑 :由通项公式变形得到,避免了组合数计算,直接通过前一项递推当前项。
适用场景 :n 中等(≤1e6)、无需取模(或模数支持除法)、需要快速打表。
时间复杂度 :O(n) (线性递推,效率极高)。
2.4 公式四:组合数差公式(正难则反) 推导逻辑 :总方案数 $\binom{2n}{n}$ 减去非法方案数 $\binom{2n}{n-1}$。
适用场景 :n 较小(≤20)、无需取模(直接计算组合数)。
时间复杂度 :O(n) (计算两个组合数)。
2.5 公式对比与选型建议 公式类型 优点 缺点 适用场景 递推公式(公式一) 无除法、逻辑直观 时间复杂度 O(n²) n≤100、模数非质数 通项公式(公式二) 组合数本质、支持大 n 需要处理除法(逆元) n≤1e5、模数为质数 线性递推(公式三) 效率最高(O(n))、代码简洁 除法可能溢出 n≤1e6、无需取模或模数支持除法 组合数差(公式四) 逻辑简单(正难则反) 组合数计算可能溢出 n≤20、直接计算(无取模)
实际应用中,优先根据数据范围 和取模要求 选择公式:小 n(≤20)选公式四;中 n(≤1e5)选公式二;大 n(≤1e6)选公式三;模数非质数选公式一。
三、卡特兰数的经典应用:从理论到实践 卡特兰数的应用场景看似零散,实则都符合'任意前缀操作数约束'的核心逻辑。以下是 6 类经典应用,帮你快速识别卡特兰数问题:
3.1 括号匹配问题 问题 :n 对括号,有多少种合法匹配方式?
示例 :n=2 时,合法方式为 (())、()(),共 2 种(对应 $C_2=2$)。
核心逻辑 :左括号数≥右括号数(任意前缀)。
3.2 栈的出栈序列问题 问题 :1,2,…,n 依次入栈,有多少种合法出栈序列?
示例 :n=3 时,合法序列为 123、132、213、231、321,共 5 种(对应 $C_3=5$)。
核心逻辑 :入栈次数≥出栈次数(任意前缀)。
3.3 购票找零问题 问题 :n 人持 50 元、n 人持 100 元购票(票价 50 元),售票员无初始零钱,有多少种排队方式能顺利找零?
示例 :n=2 时,合法排队方式为 [50,50,100,100]、[50,100,50,100],共 2 种(对应 $C_2=2$)。
核心逻辑 :50 元人数≥100 元人数(任意前缀)。
3.4 二叉树构造问题 问题 :n 个结点,能构造多少种不同的二叉搜索树(或普通二叉树)?
示例 :n=3 时,共 5 种(对应 $C_3=5$)。
核心逻辑 :根节点左子树有 i 个结点,右子树有 n-1-i 个结点,所有拆分方式累加。
3.5 圆上点配对问题 问题 :圆上有 2n 个点,两两相连且线段不交叉,有多少种配对方式?
示例 :n=3 时,共 5 种(对应 $C_3=5$)。
核心逻辑 :固定一个点,与另一个点配对,将圆分成两部分,两部分的点各自配对不交叉。
3.6 阶梯搭建问题 问题 :用 n 个 1×2 的矩形覆盖 2×n 的矩形,或用 n 个钢材搭建高度为 n 的阶梯(每步宽高均为 1),有多少种方式?
示例 :n=3 时,共 5 种(对应 $C_3=5$)。
核心逻辑 :每一步选择'横向延伸'或'纵向延伸',保证阶梯合法。
四、卡特兰数的 C++ 实现:5 道例题从入门到高阶
4.1 例题 1:矩阵 II(洛谷 P1722)—— 模数非质数,用递推公式 题目描述 :给定 n,构造 1×2n 的矩阵,放入红色(+1)和黑色(-1)算筹,要求任意前缀红色数≥黑色数,且红黑数相等,求方案数对 100 取模的结果(n≤100)。
问题本质是 n 对括号的合法匹配,即卡特兰数 $C_n$;
模数 100 不是质数,无法用逆元处理除法,选择公式一(递推公式)。
递推公式:$C_i = \sum_{j=0}^{i-1} C_j \times C_{i-j-1}$,对 100 取模。
#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 ;
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)—— 无取模,用线性递推公式 题目描述 :给定 n,计算 1,2,…,n 依次入栈的合法出栈序列数(n≤18)。
问题本质是栈的合法出栈序列,即卡特兰数 $C_n$;
选择公式三(线性递推),效率最高,代码简洁。
n≤18,卡特兰数 $C_{18}=477638700$,未超过 long long 范围(最大值 9e18)。
#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 ;
}
代码解析 :用 long long 存储结果,避免 n=18 时溢出;线性递推公式无需双重循环,时间复杂度 O(n);由于卡特兰数是整数,(4i-2) 必能被 (i+1) 整除,无需担心除法精度问题。
4.3 例题 3:球迷购票问题(洛谷 P1754)—— 无取模,用线性递推公式 题目描述 :n 人持 50 元、n 人持 100 元购票(票价 50 元),售票员无初始零钱,求合法排队方式数(n≤20)。
问题本质是购票找零,即卡特兰数 $C_n$;
n≤20,$C_{20}=6564120420$,仍在 long long 范围;
选择公式三(线性递推),代码复用性高。
#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)—— 模数为质数,用通项公式 题目描述 :2n 只小猫站成一圈,两两配对且绳子不交叉,求方案数对 1e9+7 取模的结果(n≤1e5)。
问题本质是圆上点配对,即卡特兰数 $C_n$;
n≤1e5,需高效计算,选择公式二(通项公式);
模数 1e9+7 是质数,用费马小定理求逆元,处理除法 $\frac{1}{n+1}$。
#include <iostream>
using namespace std;
typedef long long LL;
const int MOD = 1e9 + 7 ;
const int N = 2e5 + 10 ;
LL fact[N];
LL inv_fact[N];
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] = 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;
}
}
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;
}
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);
cout << catalan (n) << endl;
return 0 ;
}
代码解析 :预处理阶乘和逆元,阶乘 fact[i] 存储 $i! \pmod{MOD}$,逆元 inv_fact[i] 存储 $(i!)^{-1} \pmod{MOD}$;组合数计算通过阶乘和逆元实现;卡特兰数计算为 $C(2n, n) \times \text{inv}(n+1) \pmod{MOD}$;时间复杂度:预处理 O(n),查询 O(1),适合 n≤1e5 的场景。
4.5 例题 5:树屋阶梯(洛谷 P2532)—— 无取模,用高精度递推公式 题目描述 :用 n 个钢材搭建高度为 n 的阶梯(每步宽高均为 1),求搭建方式数(n≤500)。
问题本质是阶梯搭建,即卡特兰数 $C_n$;
n≤500,$C_{500}$ 是极大数(远超 long long 范围),需用高精度计算;
选择公式一(递推公式),规避除法,适合高精度实现。
#include <iostream>
#include <vector>
using namespace std;
const int N = 510 ;
const int M = 1000 ;
void add (vector<int >& a, const vector<int >& b) {
for (int i = 0 , t = 0 ; i < a.size () || i < b.size () || t; ++i) {
if (i == a.size ()) a.push_back (0 );
t += a[i] + (i < b.size () ? b[i] : 0 );
a[i] = t % 10 ;
t /= 10 ;
}
}
void mul (vector<int >& a, int b) {
for (int i = 0 , t = 0 ; i < a.size () || t; ++i) {
if (i == a.size ()) a.push_back (0 );
t += a[i] * b;
a[i] = t % 10 ;
t /= 10 ;
}
}
int main () {
int n;
cin >> n;
vector<vector<int >> catalan (N);
catalan[0 ].push_back (1 );
for (int i = 1 ; i <= n; ++i) {
for (int j = 0 ; j < i; ++j) {
vector<int > temp = catalan[j];
mul (temp, 1 );
}
}
return 0 ;
}
代码解析 :高精度存储用二维数组或 vector,逆序存储方便进位处理;由于公式一是乘积之和,需先计算 $C_j \times C_{i-1-j}$(高精度乘法),再累加到 $C_i$ 中(高精度加法);输出时从高位到低位遍历,跳过前导零。对于 n=500 的情况,建议使用现成的高精度库或完整实现乘法逻辑,确保正确性。
五、卡特兰数的常见误区与避坑指南
5.1 溢出问题
小 n(≤20):用 long long 存储;
中 n(≤1e5):用取模 + 逆元(公式二);
大 n(>1e5 或无取模):用高精度计算(公式一)。
5.2 公式选型错误
模数非质数:避免用公式二、三(含除法),选择公式一;
大 n 快速计算:优先用公式三(线性递推)或公式二(预处理阶乘);
小 n 直接计算:用公式四(组合数差)更直观。
5.3 问题本质识别错误
核心标志:两种操作、任意前缀一种操作数≥另一种;
反例:若无'任意前缀'约束,可能不是卡特兰数问题(如'n 对括号的所有排列数'是 $\binom{2n}{n}$,而非卡特兰数)。
5.4 高精度计算细节
存储顺序:逆序存储(低位在前),方便进位处理;
乘法进位:需双重循环遍历两个数的每一位,避免漏进位;
前导零:输出时跳过高位零,否则会输出多余的 0。
总结 通过本文的学习,你不仅能掌握卡特兰数的解法,更能理解组合数学中'抽象共性 + 具体实现'的思维方式。下次遇到括号匹配、栈操作、二叉树构造等问题时,不妨先判断是否为卡特兰数应用,再套用对应的公式和代码模板,轻松解决问题!
希望这篇内容对你有所帮助,欢迎收藏备用,后续还会分享更多组合数学经典问题(如容斥原理、隔板法),一起玩转算法!
相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online