【月度刷题计划同款】从区间 DP 到卡特兰数

题目描述

这是 LeetCode 上的 96. 不同的二叉搜索树 ,难度为 中等

Tag : 「树」、「二叉搜索树」、「动态规划」、「区间 DP」、「数学」、「卡特兰数」

给你一个整数 n ,求恰由 n 个节点组成且节点值从 1n 互不相同的 二叉搜索树 有多少种?

返回满足题意的二叉搜索树的种数。

示例 1:

ini 复制代码
输入:n = 3

输出:5

示例 2:

ini 复制代码
输入:n = 1

输出:1

提示:

  • 1 < = n < = 19 1 <= n <= 19 1<=n<=19

区间 DP

沿用 95. 不同的二叉搜索树 II 的基本思路,只不过本题不是求具体方案,而是求个数。

除了能用 95. 不同的二叉搜索树 II 提到的「卡特兰数」直接求解 n n n 个节点的以外,本题还能通过常规「区间 DP」的方式进行求解。

求数量使用 DP,求所有具体方案使用爆搜,是极其常见的一题多问搭配。

定义 f l r flr flr 为使用数值范围在 l , r l, r l,r 之间的节点,所能构建的 BST 个数

不失一般性考虑 f l r flr flr 该如何求解,仍用 l , r l, r l,r 中的根节点 i 为何值,作为切入点进行思考。

根据「BST 定义」及「乘法原理」可知: l , i − 1 l, i - 1 l,i−1 相关节点构成的 BST 子树只能在 i 的左边,而 i + 1 , r i + 1, r i+1,r 相关节点构成的 BST 子树只能在 i 的右边。所有的左右子树相互独立,因此以 i 为根节点的 BST 数量为 f l i − 1 × f i + 1 r fli - 1 \times fi + 1r fli−1×fi+1r,而 i 共有 r − l + 1 r - l + 1 r−l+1 个取值( i ∈ l , r i \in l, r i∈l,r)。

即有:
f l r = ∑ i = l r ( f l i − 1 × f i + 1 r ) flr = \sum_{i = l}^{r} (fli - 1 \times fi + 1r) flr=i=l∑r(fli−1×fi+1r)

不难发现,求解区间 l , r l, r l,rBST 数量 f l r flr flr 依赖于比其小的区间 l , i − 1 l, i - 1 l,i−1 i + 1 , r i + 1, r i+1,r,这引导我们使用「区间 DP」的方式进行递推。

不了解区间 DP 的同学,可看 前置 🧀

一些细节:由于我们 i 的取值可能会取到区间中的最值 lr,为了能够该情况下, f l i − 1 fli - 1 fli−1 f i + 1 r fi + 1r fi+1r 能够顺利参与转移,起始我们需要先对所有满足 i > = j i >= j i>=j 的 f i j fij fij 初始化为 1

Java 代码:

Java 复制代码
class Solution {
    public int numTrees(int n) {
        int[][] f = new int[n + 10][n + 10];
        for (int i = 0; i <= n + 1; i++) {
            for (int j = 0; j <= n + 1; j++) {
                if (i >= j) f[i][j] = 1;
            }
        }
        for (int len = 2; len <= n; len++) {
            for (int l = 1; l + len - 1 <= n; l++) {
                int r = l + len - 1;
                for (int i = l; i <= r; i++) {
                    f[l][r] += f[l][i - 1] * f[i + 1][r];
                }
            }
        }
        return f[1][n];
    }
}

C++ 代码:

C++ 复制代码
class Solution {
public:
    int numTrees(int n) {
        vector<vector<int>> f(n + 2, vector<int>(n + 2, 0));
        for (int i = 0; i <= n + 1; i++) {
            for (int j = 0; j <= n + 1; j++) {
                if (i >= j) f[i][j] = 1;
            }
        }
        for (int len = 2; len <= n; len++) {
            for (int l = 1; l + len - 1 <= n; l++) {
                int r = l + len - 1;
                for (int i = l; i <= r; i++) {
                    f[l][r] += f[l][i - 1] * f[i + 1][r];
                }
            }
        }
        return f[1][n];
    }
};

Python 代码:

Python 复制代码
class Solution(object):
    def numTrees(self, n):
        f = [[0] * (n + 2) for _ in range(n + 2)]
        for i in range(n + 2):
            for j in range(n + 2):
                if i >= j:
                    f[i][j] = 1
        for length in range(2, n + 1):
            for l in range(1, n - length + 2):
                r = l + length - 1
                for i in range(l, r + 1):
                    f[l][r] += f[l][i - 1] * f[i + 1][r]
        return f[1][n]

TypeScript 代码:

TypeScript 复制代码
function numTrees(n: number): number {
    const f = new Array(n + 2).fill(0).map(() => new Array(n + 2).fill(0));
    for (let i = 0; i <= n + 1; i++) {
        for (let j = 0; j <= n + 1; j++) {
            if (i >= j) f[i][j] = 1;
        }
    }
    for (let len = 2; len <= n; len++) {
        for (let l = 1; l + len - 1 <= n; l++) {
            const r = l + len - 1;
            for (let i = l; i <= r; i++) {
                f[l][r] += f[l][i - 1] * f[i + 1][r];
            }
        }
    }
    return f[1][n];
};
  • 时间复杂度: O ( n 3 ) O(n^3) O(n3)
  • 空间复杂度: O ( n 2 ) O(n^2) O(n2)

区间 DP(优化)

求解完使用 1 , n 1, n 1,n n n n 个连续数所能构成的 BST 个数后,再来思考一个问题:使用 L , R L, R L,R n = R − L + 1 n = R - L + 1 n=R−L+1 个连续数,所能构成的 BST 个数又是多少。

答案是一样的。

n n n 个连续数构成的 BST 个数仅与数值个数有关系,与数值大小本身并无关系

由于可知,我们上述的「区间 DP」必然进行了大量重复计算,例如 f 1 3 f13 f13 f 2 4 f24 f24 同为大小为 3 3 3 的区间,却被计算了两次。

调整我们的状态定义:定义 f k fk fk 为考虑连续数个数为 k k k 时,所能构成的 BST 的个数

不失一般性考虑 f i fi fi 如何计算,仍用 1 , i 1, i 1,i 中哪个数值作为根节点进行考虑。假设使用数值 j j j 作为根节点,则有 f j − 1 × f i − j fj - 1 \times fi - j fj−1×fi−jBST 可贡献到 f i fi fi,而 j j j 共有 i i i 个取值( j ∈ 1 , i j \in 1, i j∈1,i)。

即有:
f i = ∑ j = 1 i ( f j − 1 × f i − j ) fi = \sum_{j = 1}^{i}(fj - 1 \times fi - j) fi=j=1∑i(fj−1×fi−j)

同时有初始化 f 0 = 1 f0 = 1 f0=1,含义为没有任何连续数时,只有"空树"一种合法方案。

Java 代码:

Java 复制代码
class Solution {
    public int numTrees(int n) {
        int[] f = new int[n + 10];
        f[0] = 1;
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= i; j++) {
                f[i] += f[j - 1] * f[i - j];
            }
        }
        return f[n];
    }
}

C++ 代码:

C++ 复制代码
class Solution {
public:
    int numTrees(int n) {
        vector<int> f(n + 10, 0);
        f[0] = 1;
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= i; j++) {
                f[i] += f[j - 1] * f[i - j];
            }
        }
        return f[n];
    }
};

Python 代码:

Python 复制代码
class Solution:
    def numTrees(self, n: int) -> int:
        f = [0] * (n + 10)
        f[0] = 1
        for i in range(1, n + 1):
            for j in range(1, i + 1):
                f[i] += f[j - 1] * f[i - j]
        return f[n]

TypeScript 代码:

TypeScript 复制代码
function numTrees(n: number): number {
    const f = new Array(n + 10).fill(0);
    f[0] = 1;
    for (let i = 1; i <= n; i++) {
        for (let j = 1; j <= i; j++) {
            f[i] += f[j - 1] * f[i - j];
        }
    }
    return f[n];
};
  • 时间复杂度: O ( n 2 ) O(n^2) O(n2)
  • 空间复杂度: O ( n ) O(n) O(n)

数学 - 卡特兰数

在「区间 DP(优化)」中的递推过程,正是"卡特兰数"的 O ( n 2 ) O(n^2) O(n2) 递推过程。通过对常规「区间 DP」的优化,我们得证 95. 不同的二叉搜索树 II 中「给定 n n n 个节点所能构成的 BST 的个数为卡特兰数」这一结论。

对于精确求卡特兰数,存在时间复杂度为 O ( n ) O(n) O(n) 的通项公式做法,公式为 C n + 1 = C n ⋅ ( 4 n + 2 ) n + 2 C_{n+1} = \frac{C_n \cdot (4n + 2)}{n + 2} Cn+1=n+2Cn⋅(4n+2)。

Java 代码:

Java 复制代码
class Solution {
    public int numTrees(int n) {
        if (n <= 1) return 1;
        long ans = 1;
        for (int i = 0; i < n; i++) ans = ans * (4 * i + 2) / (i + 2);
        return (int)ans;
    }
}

C++ 代码:

C++ 复制代码
class Solution {
public:
    int numTrees(int n) {
        if (n <= 1) return 1;
        long long ans = 1;
        for (int i = 0; i < n; i++) ans = ans * (4 * i + 2) / (i + 2);
        return (int)ans;
    }
};

Python 代码:

Python 复制代码
class Solution:
    def numTrees(self, n: int) -> int:
        if n <= 1:
            return 1
        ans = 1
        for i in range(n):
            ans = ans * (4 * i + 2) // (i + 2)
        return ans

TypeScript 代码:

TypeScript 复制代码
function numTrees(n: number): number {
    if (n <= 1) return 1;
    let ans = 1;
    for (let i = 0; i < n; i++) ans = ans * (4 * i + 2) / (i + 2);
    return ans;
};
  • 时间复杂度: O ( n ) O(n) O(n)
  • 空间复杂度: O ( 1 ) O(1) O(1)

最后

这是我们「刷穿 LeetCode」系列文章的第 No.96 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:github.com/SharingSour...

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。

更多更全更热门的「笔试/面试」相关资料可访问排版精美的 合集新基地 🎉🎉

相关推荐
8Qi81 小时前
LeetCode 516:最长回文子序列
算法·leetcode·职场和发展·动态规划
IT_陈寒1 小时前
Redis持久化这个坑,我爬了一整天才出来
前端·人工智能·后端
无风听海1 小时前
多租户系统中的 OIDC:Discovery 端点与联合登录的深度实践
后端·python·flask
mONESY2 小时前
JavaScript 栈、队列、数组与链表核心知识点总结
javascript·面试
贺国亚2 小时前
电商AI辅助交易场景
面试
小小前端仔LC2 小时前
Node.js + LangChain + React:搭建个人知识库(六)- “吃什么”项目实战:从700+菜谱入库到Taro H5端JSON渲染
前端·后端
youngerwang2 小时前
【从搬运工到协处理器:网卡芯片架构、算法、验证与边缘演进深度剖析】
网络·算法·架构·芯片
chase_my_dream2 小时前
C++ + SLAM 高频面试问题整理
开发语言·c++·面试
想要成为糕糕手2 小时前
前端必修课:JavaScript 数组与数据结构底层逻辑全解析
javascript·数据结构·面试
程序员黑豆2 小时前
AI全栈开发之Java:怎么配置Java环境变量
前端·后端·ai编程