你有没有想过:n 对括号有多少种合法匹配方式?1,2,3…n 依次入栈,有多少种合法出栈序列?圆上 2n 个点两两相连,怎样保证线段不交叉?这些看似毫无关联的问题,背后都指向同一个神奇的数列 ——卡特兰数。 卡特兰数(Catalan Number)是组合数学中的 “明星数列”,以比利时数学家欧仁・夏尔・卡塔兰命名。它就像一把万能钥匙,能解锁无数计数难题,不仅是算法面试的 “高频考点”(字节、阿里、腾讯等大厂笔试常客),还广泛应用于括号匹配、栈操作、二叉树构造、路径规划等场景。 本文将带你揭开卡特兰数的神秘面纱:从生活场景切入,推导核心公式,拆解经典应用,再通过 5 道梯度例题(从入门到高阶),手把手教你用 C++ 实现高效解法。无论你是算法新手,还是想巩固组合数学的开发者,读完这篇文章,都能彻底掌握卡特兰数的核心逻辑,轻松应对各类相关问题!
卡特兰数是一组满足特定递推关系的整数序列,记为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,因此大规模计算时需注意溢出问题(通常用取模或高精度处理)。
卡特兰数的核心本质是:在两种对立操作的序列中,保证任意前缀中一种操作数不小于另一种操作数。
比如:
这些问题的共性,正是卡特兰数的应用场景标志。
卡特兰数有 4 种常用公式,分别适用于不同场景(如数据范围、是否取模、是否需要递推打表),掌握它们就能应对所有卡特兰数问题。

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

;

;
适用场景:n 较小(≤100)、需要规避除法(如取模的模数不是质数)。
时间复杂度:O(n^2)。

推导逻辑:从 2n 个位置中选择 n 个位置放置 “左操作”(如左括号),总方案数为(n2n);再减去非法方案数(任意前缀中 “右操作” 多于 “左操作”),最终得到上述通项公式(推导过程见下文 “经典问题”)。
适用场景:n 中等(≤1e5)、模数为质数(可通过逆元处理除法)。
时间复杂度:O(n)(预处理阶乘和逆元后,单次查询O(1))。

推导逻辑:由通项公式变形得到,避免了组合数计算,直接通过前一项递推当前项。
适用场景:n 中等(≤1e6)、无需取模(或模数支持除法)、需要快速打表。
时间复杂度:O(n)(线性递推,效率极高)。

推导逻辑:总方案数(n2n)减去非法方案数(n−12n)(非法方案数的证明见下文 “经典问题”)。
适用场景:n 较小(≤20)、无需取模(直接计算组合数)。
时间复杂度:O(n)(计算两个组合数)。
公式类型 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
递推公式(公式一) | 无除法、逻辑直观 | 时间复杂度O(n2) | n≤100、模数非质数 |
通项公式(公式二) | 组合数本质、支持大 n | 需要处理除法(逆元) | n≤1e5、模数为质数 |
线性递推(公式三) | 效率最高(O(n))、代码简洁 | 除法可能溢出 | n≤1e6、无需取模或模数支持除法 |
组合数差(公式四) | 逻辑简单(正难则反) | 组合数计算可能溢出 | n≤20、直接计算(无取模) |
实际应用中,优先根据数据范围和取模要求选择公式:
卡特兰数的应用场景看似零散,实则都符合 “任意前缀操作数约束” 的核心逻辑。以下是 6 类经典应用,帮你快速识别卡特兰数问题:
问题:n 对括号,有多少种合法匹配方式?
示例:n=2 时,合法方式为(())、()(),共 2 种(对应C2=2)。
核心逻辑:左括号数≥右括号数(任意前缀)。
问题:1,2,…,n 依次入栈,有多少种合法出栈序列?
示例:n=3 时,合法序列为 123、132、213、231、321,共 5 种(对应C3=5)。
核心逻辑:入栈次数≥出栈次数(任意前缀)。
问题:n 人持 50 元、n 人持 100 元购票(票价 50 元),售票员无初始零钱,有多少种排队方式能顺利找零?
示例:n=2 时,合法排队方式为 [50,50,100,100]、[50,100,50,100],共 2 种(对应C2=2)。
核心逻辑:50 元人数≥100 元人数(任意前缀)。
问题:n 个结点,能构造多少种不同的二叉搜索树(或普通二叉树)?
示例:n=3 时,共 5 种(对应C3=5)。
核心逻辑:根节点左子树有 i 个结点,右子树有 n-1-i 个结点,所有拆分方式累加。
问题:圆上有 2n 个点,两两相连且线段不交叉,有多少种配对方式?
示例:n=3 时,共 5 种(对应C3=5)。
核心逻辑:固定一个点,与另一个点配对,将圆分成两部分,两部分的点各自配对不交叉。
问题:用 n 个 1×2 的矩形覆盖 2×n 的矩形,或用 n 个钢材搭建高度为 n 的阶梯(每步宽高均为 1),有多少种方式?
示例:n=3 时,共 5 种(对应C3=5)。
核心逻辑:每一步选择 “横向延伸” 或 “纵向延伸”,保证阶梯合法。
题目链接:https://www.luogu.com.cn/problem/P1722

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

,对 100 取模。
C++ 代码实现
#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;题目链接:https://www.luogu.com.cn/problem/P1044

题目描述:给定 n,计算 1,2,…,n 依次入栈的合法出栈序列数(n≤18)。
输入示例:3 → 输出示例:5
解题思路:

=477638700,未超过long long范围(最大值 9e18);
C++ 代码实现
#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 时溢出;题目链接:https://www.luogu.com.cn/problem/P1754

题目描述:n 人持 50 元、n 人持 100 元购票(票价 50 元),售票员无初始零钱,求合法排队方式数(n≤20)。
输入示例:2 → 输出示例:2
解题思路:
long long范围;C++ 代码实现
#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;
}代码解析:
题目链接https://www.luogu.com.cn/problem/P1375

题目描述:2n 只小猫站成一圈,两两配对且绳子不交叉,求方案数对 1e9+7 取模的结果(n≤1e5)。
输入示例:3 → 输出示例:5
解题思路:
C++ 代码实现
#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 的逆元(用快速幂计算);题目链接:https://www.luogu.com.cn/problem/P2532

题目描述:用 n 个钢材搭建高度为 n 的阶梯(每步宽高均为 1),求搭建方式数(n≤500)。
输入示例:3 → 输出示例:5
解题思路:
long long范围),需用高精度计算;C++ 代码实现
#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中(高精度加法);long long存储;通过本文的学习,你不仅能掌握卡特兰数的解法,更能理解组合数学中 “抽象共性 + 具体实现” 的思维方式。下次遇到括号匹配、栈操作、二叉树构造等问题时,不妨先判断是否为卡特兰数应用,再套用对应的公式和代码模板,轻松解决问题! 如果本文对你有帮助,欢迎点赞、收藏、转发,也欢迎在评论区交流讨论~ 后续还会分享更多组合数学经典问题(如容斥原理、隔板法),关注我,一起玩转算法!