96. Unique Binary Search Trees**

96. Unique Binary Search Trees**

https://leetcode.com/problems/unique-binary-search-trees/

题目描述

Given n, how many structurally unique BST’s (binary search trees) that store values 1 ... n?

Example:

Input: 3
Output: 5
Explanation:
Given n = 3, there are a total of 5 unique BST's:

   1         3     3      2      1
    \       /     /      / \      \
     3     2     1      1   3      2
    /     /       \                 \
   2     1         2                 3

C++ 实现 1

记忆化搜索. 这道题可以这样考虑: 对于 nums = [1, 2, ...., n] 这个数组, 要组成 BST, 可以计算 i 为根节点能构建的 BST 个数 count(i), 然后对 count 求和即可. 现在假设 n = 5, 问以 3 为根节点的 BST 有多少个? 其个数是子序列 [1, 2] 能构建的左子树个数 A 乘上子序列 [4, 5] 能构建的右子树个数 B 的乘积 (A * B). 但需要注意到的是, [1, 2][4, 5] 看起来是不同的, 但它们的相似之处是它们都是有序的. 如果我们忽略数值本身的大小, 单单比较这两个数组能构建的 BST 个数, 我们会发现它们能构建的 BST 个数也是相同的. 因此, 一个有序数组能构建的 BST 个数不取决于它数值本身, 而是取决于它的元素个数. 当我们依次遍历 nums 时, 计算以 i 为根节点的 BST 个数, 就是求 [1, ..., i - 1] 以及 [i + 1, ..., n] 两个序列能构成的 BST 个数的乘积, 前者有 i - 1 个元素, 后者有 n - i 个元素, 转换为求子问题.

class Solution {
private:
    unordered_map<int, int> record;
public:
    int numTrees(int n) {
        if (n <= 0) return 1;
        if (record.count(n)) return record[n];
        if (n <= 2) {
            record[n] = n;
            return n;
        }
        int sum = 0;
        for (int i = 1; i <= n; ++ i) {
            sum += numTrees(i - 1) * numTrees(n - i);
        }
        record[n] = sum;
        return sum;
    }
};

提交完之后发现 2 年前写这道题采用动态规划的方法, 用的是参考的代码提交的. 突然之间我有种 feeling, 感觉这次我的分析能力有了很大的进步, 但对动态规划本身并不敏感. 做完这道题之后, 思考为什么这种做法这么神奇, 不会出错, 到底拿什么保证结果的正确性. 我发现纠结于迭代或者递归的细节意义不大, 里面有无穷种可能, 多递归几次人就晕了. 本质上, 是这道题存在重复的子问题, 我们通过观察以及分析, 发现要解决原始问题, 可以先处理子问题; 同时又发现子问题和原问题具有相同的结构; 注意有的时候, 子问题包装的很好, 不容易发现和原问题的联系, 比如上面问题中, 如果纠结于子数组中的具体数值而不是个数, 那么就不容易抓住问题的本质. 最后使用记忆化搜索, 是考虑到进行递归时很多结果已经被计算过, 为了避免重复计算, 可以用额外空间保存已经计算过的结果. 另外, 对动态规划本身敏不敏感, 不是说这道题做过, 或者看过类似的题, 又或者这类问题是计数类型的, 这偏离了本质, 而是在分析问题时, 发现重复子问题, 就自然而然的想到了对应的解法, 而这种思考问题的方式被大家称为动态规划.

C++ 实现 2

动态规划. 下面参考:

G(n) 为给定 n 时可以生成的 BST 的个数, 那么, 如果以数 i 为根节点, 那么可以递归的以 [1...i - 1] 中的数生成左子树, 以 [i+1...n] 中的数生成右子树, 注意 G(n) 的定义是给定 n 个数可以生成的 BST 的个数, 那么 [1...i-1] 中有 i - 1 个数, 就可以生成 G(i - 1) 个 BST, 而 [i+1...n] 中有 n - i 个数, 因此可以生成 G(n - i) 个 BST.

由于 i 可以从 1 变化到 n, 因此:(考虑 0 的话, 有 G(0) == 1, 因为空树也相当于一棵 BST)

C 0 = 1 C n + 1 = ∑ i = 0 n C i C n − i ( n ≥ 0 ) C_0 = 1 \quad C_{n+1} = \sum_{i=0}^{n} C_iC_{n - i} \quad (n \geq 0) C0=1Cn+1=i=0nCiCni(n0)

代码如下:

class Solution {
public:
    int numTrees(int n) {
        vector<int> G(n + 1, 0);
        G[0] = 1;
        G[1] = 1;
      	// 此处的 i 就相当于公式中的 n + 1
        for (int i = 2; i < n + 1; ++i) {
          	// 使用下面这个for 循环进行求和.
            for (int j = 0; j < i; ++j) {
                G[i] += G[j] * G[i - 1 - j];
            }
        }
        return G[n];
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值