学习卡特兰数:从原理到应用,解决所有递推计数问题

学习卡特兰数:从原理到应用,解决所有递推计数问题

在组合数学和算法面试中,有一类问题始终占据重要地位------它们的答案都指向同一个数列:卡特兰数。无论是"合法括号组合""出栈序列计数",还是"二叉搜索树形态数",背后都藏着卡特兰数的逻辑。

本文将从卡特兰数的由来出发,用通俗的语言拆解核心原理(递推公式+通项公式推导),再结合3类经典应用场景,给出可直接套用的解题模板,帮你彻底掌握这一高频考点。

一、卡特兰数的由来:一个赌徒的问题

卡特兰数(Catalan Number)最早由比利时数学家欧仁·查理·卡特兰(Eugène Charles Catalan)在1838年提出,但在此之前,欧拉、兰伯特等数学家已在研究相关问题。

其最初的起源是"凸多边形三角剖分问题":一个凸n边形,通过不相交的对角线,能将其分成多少个三角形?

比如:

  • 凸3边形(三角形):只有1种分法(本身就是三角形)

  • 凸4边形:有2种分法(沿两条不同的对角线拆分)

  • 凸5边形:有5种分法

这些分法数,正是卡特兰数的前几项。后来人们发现,这个数列还能解决大量"约束条件下的计数问题",成为组合数学中的核心数列之一。

二、卡特兰数的核心原理:两个公式的通俗推导

卡特兰数的核心是两个公式:递推公式(适合小范围计算、理解逻辑)和通项公式(适合大范围计算、提高效率)。我们从最直观的"合法括号组合"问题入手,一步步推导这两个公式。

2.1 先明确:什么是"合法括号组合"?

给定n对括号,合法组合需满足两个条件:

  1. 任意前缀中,左括号数量 ≥ 右括号数量(过程约束,避免"右括号超前");

  2. 最终左括号总数 = 右括号总数(结果约束,用完所有括号)。

比如n=2时,合法组合有2种:()()(());非法组合有2种:)()())(

2.2 递推公式推导:拆分问题,化繁为简

递推公式的核心思想是:把大问题拆成两个独立的小问题,用"乘法原理+加法原理"合并结果

第一步:找拆分的"锚点"

对于任意一个n对括号的合法组合,第一个左括号(必然有一个唯一对应的右括号)。这个对应关系就是我们的"拆分锚点",它将整个组合分成3部分:

( 内部i对括号 ) 右边n-1-i对括号

解释:

  • 外层的()是锚点,用掉了1对括号;

  • "内部i对括号":锚点中间的合法组合,方案数记为C(i)(第i个卡特兰数);

  • "右边n-1-i对括号":锚点右边的合法组合,方案数记为C(n-1-i);

  • i的取值范围是0≤i≤n-1(内部可以是空的,也可以塞下n-1对括号)。

第二步:用乘法原理算单种i的方案数

乘法原理:做一件事分两步,第一步有A种方法,第二步有B种方法,总方法数=A×B。

对应拆分逻辑:

  • 第一步:选内部i对括号的组合,有C(i)种方法;

  • 第二步:选右边n-1-i对括号的组合,有C(n-1-i)种方法;

  • 单种i对应的总方案数 = C(i) × C(n-1-i)。

第三步:用加法原理算所有i的总方案数

加法原理:做一件事有多种互斥路径,每种路径有A、B、C种方法,总方法数=A+B+C。

不同i对应的组合是"互斥"的(一个组合只能属于某一个i),因此总方案数是所有i的方案数之和。

最终递推公式

边界条件:C(0) = 1(0对括号只有1种空方案)

递推公式:C(n) = C(0)×C(n-1) + C(1)×C(n-2) + ... + C(n-1)×C(0) (n≥1),即从i=0到i=n-1,将C(i)与C(n-1-i)的乘积依次相加

验证:用递推公式算前几项

  • C(0) = 1

  • C(1) = C(0)×C(0) = 1×1 = 1(1对括号:()

  • C(2) = C(0)×C(1) + C(1)×C(0) = 1×1 + 1×1 = 2(2对括号:()()(())

  • C(3) = C(0)×C(2) + C(1)×C(1) + C(2)×C(0) = 1×2 + 1×1 + 2×1 = 5(3对括号:5种合法组合)

完全符合实际情况!

2.3 通项公式推导:补集思想,简化计算

递推公式的时间复杂度是O(n²),n较大时效率低。我们需要更简洁的通项公式,这里用"补集思想"推导:合法组合数 = 总排列数 - 非法组合数

第一步:计算总排列数

n对括号 = n个( + n个),共2n个字符。从2n个位置中选n个放(,剩下的放),总排列数是组合数:从2n个位置里选n个位置的选法数,记为C(2n, n),其计算方式是(2n的阶乘)除以(n的阶乘乘以n的阶乘)。

第二步:计算非法组合数

非法组合的定义:存在某前缀,右括号数 > 左括号数。这里有个关键技巧------非法组合与"n+1个 ( ** + n-1个** ) 的排列"一一对应

转换方法:找到第一个右括号数超过左括号数的位置,将这个位置及之前的所有括号反转(())()。

例子:n=2,非法组合)()

  • 第一个非法位置是第1位(右括号数=1,左括号数=0);

  • 反转第1位:)(,得到新排列(()(

  • 新排列有3个(、1个)(即n+1个(、n-1个))。

因此,非法组合数 = 从2n个位置选n+1个放(的组合数:

因此,非法组合数 = 从2n个位置选n+1个放(的组合数,记为C(2n, n+1),其计算方式是(2n的阶乘)除以((n+1)的阶乘乘以(n-1)的阶乘)。

第三步:化简得到通项公式

合法组合数 = 总排列数 - 非法组合数:

合法组合数 = 总排列数 - 非法组合数,对应关系为:C(n) = C(2n, n) - C(2n, n+1)(C(2n, n)是总排列数,C(2n, n+1)是非法组合数)。

代入组合数公式化简(过程略,核心是通分、约分):

代入组合数公式化简(过程略,核心是通分、约分)后,得到通项公式:C(n) = 1/(n+1) × C(2n, n),其计算方式是(2n的阶乘)除以((n+1)的阶乘乘以n的阶乘)。

验证:用通项公式算C(3)

用通项公式验证C(3):C(3) = 1/(3+1) × C(6, 3),其中C(6, 3)是从6个位置选3个的组合数,结果为20,因此C(3) = 1/4 × 20 = 5。

和递推公式结果一致!

2.4 卡特兰数的前10项(快速对照)

根据公式计算,前10项卡特兰数为:

n 0 1 2 3 4 5 6 7 8 9
C(n) 1 1 2 5 14 42 132 429 1430 4862

三、卡特兰数的3类经典应用场景(附解题模板)

所有卡特兰数的应用场景,都具备一个核心特征:存在"前缀约束",且问题可拆分为两个独立子问题。下面介绍3类面试高频场景,均提供可直接套用的代码模板。

3.1 场景1:合法括号生成(最经典)

问题描述

给定n对括号,生成所有合法的括号组合。

解题思路

用回溯法,严格遵循"前缀约束":

  • 左括号数≤n时,可添加左括号;

  • 右括号数<左括号数时,可添加右括号。

代码模板(JavaScript)

javascript 复制代码
/**
 * 生成n对合法括号
 * @param {number} n - 括号对数
 * @returns {string[]} 所有合法组合
 */
function generateParenthesis(n) {
    const result = [];

    // 回溯函数:cur-当前组合,left-已用左括号数,right-已用右括号数
    const backtrack = (cur, left, right) => {
        // 终止条件:组合长度=2n
        if (cur.length === 2 * n) {
            result.push(cur);
            return;
        }

        // 可添加左括号(左括号未用完)
        if (left < n) {
            backtrack(cur + '(', left + 1, right);
        }

        // 可添加右括号(右括号数<左括号数)
        if (right < left) {
            backtrack(cur + ')', left, right + 1);
        }
    };

    backtrack('', 0, 0);
    return result;
}

// 测试:n=3 → 输出5种组合
console.log(generateParenthesis(3)); 
// ["((()))","(()())","(())()","()(())","()()()"]
}

3.2 场景2:合法出栈序列

问题描述

给定进栈序列1~n,生成所有合法的出栈序列(不能从空栈出栈)。

解题思路(与括号问题的等价性)

出栈场景 括号场景 约束条件
元素进栈 添加左括号 进栈数≤n
元素出栈 添加右括号 出栈数<进栈数

代码模板(JavaScript)

javascript 复制代码
/**
 * 生成1~n的所有合法出栈序列
 * @param {number} n - 元素个数
 * @returns {number[][]} 所有合法出栈序列
 */
function generatePopSequence(n) {
    const result = [];

    // 回溯函数:inStack-栈内元素,outStack-已出栈元素,next-下一个要进栈的元素
    const backtrack = (inStack, outStack, next) => {
        // 终止条件:栈空且所有元素已进栈
        if (inStack.length === 0 && next > n) {
            result.push([...outStack]);
            return;
        }

        // 栈非空时,可出栈
        if (inStack.length > 0) {
            const top = inStack.pop();
            outStack.push(top);
            backtrack(inStack, outStack, next);
            // 回溯:恢复状态
            outStack.pop();
            inStack.push(top);
        }

        // 还有元素未进栈时,可进栈
        if (next <= n) {
            inStack.push(next);
            backtrack(inStack, outStack, next + 1);
            // 回溯:恢复状态
            inStack.pop();
        }
    };

    backtrack([], [], 1);
    return result;
}

// 测试:n=3 → 输出5种序列
console.log(generatePopSequence(3)); 
// [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,2,1]]
}

3.3 场景3:不同的二叉搜索树(BST)形态

问题描述

给定n个不同的节点(如1~n),统计所有可能的二叉搜索树形态数,或生成所有形态。

解题思路

BST的核心是"左子树值<根值<右子树值",因此:

  • 选第i个节点作为根,左子树有i-1个节点,右子树有n-i个节点;

  • 左子树形态数×右子树形态数 = 以i为根的形态数;

  • 遍历所有i(1≤i≤n),求和得到总形态数(即C(n))。

代码模板1:统计形态数(递推公式)

javascript 复制代码
/**
 * 统计n个节点的BST形态数
 * @param {number} n - 节点数
 * @returns {number} 形态总数(第n个卡特兰数)
 */
function countBST(n) {
    const dp = new Array(n + 1).fill(0);
    dp[0] = 1; // 边界:0个节点=空树,1种形态

    // 递推公式:C(n) = sum(C(i) * C(n-1-i))
    for (let i = 1; i <= n; i++) {
        for (let j = 0; j < i; j++) {
            // j=左子树节点数,i-1-j=右子树节点数(根占1个)
            dp[i] += dp[j] * dp[i - 1 - j];
        }
    }
    return dp[n];
}

// 测试:n=3 → 输出5
console.log(countBST(3)); 
}

代码模板2:生成所有BST形态

javascript 复制代码
// 定义二叉树节点
class TreeNode {
    constructor(val) {
        this.val = val;
        this.left = null;
        this.right = null;
    }
}

/**
 * 生成n个节点的所有BST形态
 * @param {number} n - 节点数(值为1~n)
 * @returns {TreeNode[]} 所有BST的根节点数组
 */
function generateBST(n) {
    // 生成[start, end]范围内的所有BST
    const build = (start, end) => {
        const res = [];
        if (start > end) {
            res.push(null); // 空树
            return res;
        }

        // 遍历所有可能的根节点i
        for (let i = start; i <= end; i++) {
            const leftTrees = build(start, i - 1); // 左子树所有形态
            const rightTrees = build(i + 1, end); // 右子树所有形态

            // 乘法原理:组合左右子树
            for (const left of leftTrees) {
                for (const right of rightTrees) {
                    const root = new TreeNode(i);
                    root.left = left;
                    root.right = right;
                    res.push(root);
                }
            }
        }
        return res;
    };

    return n === 0 ? [] : build(1, n);
}

// 测试:统计n=3的形态数
console.log(generateBST(3).length); // 输出5
}

四、如何快速判断题目是否用卡特兰数?

遇到以下特征的计数/生成题,直接联想到卡特兰数:

  1. 存在"前缀约束"(如括号左≥右、栈非空才能出栈、BST的左<根<右);

  2. 问题可拆分为"左右两个独立子问题";

  3. 小例子的结果符合卡特兰数(n=1→1,n=2→2,n=3→5,n=4→14)。

五、总结

卡特兰数的核心不是"死记公式",而是"理解约束下的拆分逻辑":

  1. 由来:源于凸多边形三角剖分问题,后来扩展到各类约束计数场景;

  2. 原理:递推公式(拆分问题+乘法/加法原理)、通项公式(补集思想+组合数化简);

  3. 应用:括号生成、出栈序列、BST形态等,核心是"前缀约束+子问题拆分";

  4. 解题:通用模板基于回溯或递推,只需根据场景调整"约束条件"和"子问题组合方式"。

掌握卡特兰数,就能解决一类高频面试题------记住:所有符合"前缀约束+子问题拆分"的计数问题,答案都是卡特兰数

拓展练习:尝试用卡特兰数解决"凸n边形三角剖分"问题,验证是否符合C(n-2)(提示:凸n边形的三角剖分数=第n-2个卡特兰数)。

六、进阶优化技巧与面试真题解析

6.1 进阶优化:大数情况下的卡特兰数计算

卡特兰数增长极快,当n≥20时,数值会远超JavaScript原生Number类型的最大值(2⁵³-1),导致精度丢失。此时需要通过"大数运算"或借助Python的原生大整数类型解决。

优化方案1:JavaScript大数运算(模拟乘法)

Plain 复制代码
/**
 * 大数运算计算第n个卡特兰数(避免精度丢失)
 * @param {number} n - 卡特兰数序号
 * @returns {string} 第n个卡特兰数(字符串形式,避免大数溢出)
 */
function catalanBigNumber(n) {
    // 用数组存储大数,低位在前
    let result = [1];
    // 计算 (2n)! / (n! * (n+1)!) = 乘积从i=n+2到2n,除以i-n
    for (let i = n + 2; i <= 2 * n; i++) {
        // 第一步:乘以i
        let carry = 0;
        for (let j = 0; j < result.length; j++) {
            const product = result[j] * i + carry;
            result[j] = product % 10;
            carry = Math.floor(product / 10);
        }
        while (carry > 0) {
            result.push(carry % 10);
            carry = Math.floor(carry / 10);
        }
        // 第二步:除以 (i - n)
        let remainder = 0;
        for (let j = result.length - 1; j >= 0; j--) {
            const dividend = remainder * 10 + result[j];
            result[j] = Math.floor(dividend / (i - n));
            remainder = dividend % (i - n);
        }
        // 移除前面的0
        while (result.length > 1 && result[result.length - 1] === 0) {
            result.pop();
        }
    }
    // 数组反转,转为字符串
    return result.reverse().join('');
}

// 测试:n=20 → 输出16796(正确的第20个卡特兰数)
console.log(catalanBigNumber(20)); // 输出"6564120420"
}

优化方案2:Python原生大整数(简洁高效)

Python的int类型支持任意大小整数,无需额外处理大数,直接用通项公式计算即可:

Plain 复制代码
def catalan(n):
    from math import comb
    # 通项公式:C(n) = 1/(n+1) * C(2n, n)
    return comb(2 * n, n) // (n + 1)

# 测试:n=100 → 直接输出超大卡特兰数
print(catalan(100))
# 输出:896519947090131496687170070074100632420837521538745909320

6.2 面试真题解析

真题1:LeetCode 22. 括号生成(中等)

题目描述:数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。

解题思路:直接套用"合法括号生成"模板,核心是回溯时遵循"左括号数≤n"和"右括号数<左括号数"的约束。

代码实现:与本文3.1节的代码模板完全一致,提交即可通过。

真题考点:卡特兰数的核心约束、回溯算法的应用。

真题2:LeetCode 96. 不同的二叉搜索树(中等)

题目描述:给你一个整数 n ,求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。

解题思路:本题是卡特兰数的直接应用,n个节点的BST形态数就是第n个卡特兰数,直接套用3.3节的"统计形态数"模板。

Plain 复制代码
/**
 * @param {number} n
 * @returns {number}
 */
var numTrees = function(n) {
    const dp = new Array(n + 1).fill(0);
    dp[0] = 1;
    for (let i = 1; i <= n; i++) {
        for (let j = 0; j < i; j++) {
            dp[i] += dp[j] * dp[i - 1 - j];
        }
    }
    return dp[n];
};

真题考点:卡特兰数递推公式、动态规划思想。

真题3:LeetCode 95. 不同的二叉搜索树 II(中等)

题目描述:给你一个整数 n ,请你生成所有由 1 到 n 为节点所组成的 二叉搜索树 ,并返回它们的根节点列表。

解题思路:套用3.3节的"生成所有BST形态"模板,通过递归构建左右子树,再组合得到所有合法BST。

代码实现:与本文3.3节的代码模板一致,注意TreeNode的定义符合题目要求即可。

真题考点:卡特兰数的拆分逻辑、递归组合子问题。

相关推荐
愚坤6 分钟前
前端真有意思,又干了一年图片编辑器
前端·javascript·产品
文心快码BaiduComate10 分钟前
用Comate开发我的第一个MCP——让Vibe Coding长长脑子
前端·后端·程序员
OpenTiny社区36 分钟前
这是OpenTiny与开发者一起写下的2025答卷!
前端·javascript·vue.js
山楂树の1 小时前
买卖股票的最佳时机(动态规划)
算法·动态规划
龙在天1 小时前
复刻网页彩虹🌈镭射效果
前端
孟祥_成都1 小时前
让 AI 自动写 SQL、读文档,前端也能玩转 Agent! langchain chains 模块解析
前端·人工智能
小O的算法实验室1 小时前
2024年IEEE TMC SCI1区TOP,面向无人机辅助 MEC 系统的轨迹规划与任务卸载的双蚁群算法,深度解析+性能实测
算法·无人机·论文复现·智能算法·智能算法改进
天蓝色的鱼鱼2 小时前
别再瞎转Base64了!一文打通前端二进制任督二脉
前端
哟哟耶耶2 小时前
Plugin-安装Vue.js devtools6.6.3扩展(组件层级可视化)
前端·javascript·vue.js
梦6502 小时前
【前端实战】图片元素精准定位:无论缩放,元素始终钉在指定位置
前端·html·css3