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
动态规划. 下面参考:
- DP Solution in 6 lines with explanation. F(i, n) = G(i-1) * G(n-i)
- http://www.cnblogs.com/grandyang/p/4299608.html
设 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=0∑nCiCn−i(n≥0)
代码如下:
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];
}
};