给定一个整数 n,求以 1 … n 为节点组成的二叉搜索树有多少种?
虽然暴力破解会超时,但是后面动态规划的思路基础是基于这里的暴力破解,所以建议不明白暴力破解的同学认真看看这里的方法。
BST分为三个部分:1)root节点;2)小于root的节点;3)大于root的节点;所以给定节点[1, 2, 3, …, n -1, n],选取任意一个节点为root节点,那么整个树的结构可以概括为:
当固定root后,剩余的树节点划分为两个部分。之后继续对剩余两个部分执行上述的步骤,直到某个部分只剩下1个节点或空节点则停止。那么n个节点形成的BST总个数则依次为:1~n分别为root的情况的总和。到这里,我们理解了递归的大部分思路,现在还剩下一个问题:为什么某个节点的BST总数关系是:左节点总数 * 右节点总数 ?回到上面的例子,来看看左右节点所能形成的BST总数:
我们发现1个节点只能形成1种BST,两个节点可以形成2种BST,那么3为root的情况下,所能形成的BST总数为左右两边个数的乘积,即A分别和B、C进行组合;此部分对应代码:2.2.1 暴力破解(Brute Force, Time Limit Exceeded)
通过上面的分析,我们发现节点所能构成的BST总数与具体的节点无关,而与多少个节点有关。换句话说,[3,4]和[5,6]构成的BST总数都是2,以此类推,只要构成树的节点数相同,那么所能构建的树的总数也相同。所以基于这点就能很自然的想到用记忆方法,存储之前计算过的值,降低时间复杂度。此部分对应代码:2.2.2 记忆(Memorization)
2.2.1 暴力破解(Brute Force, Time Limit Exceeded)
class Solution {
int recursionMethod(int left, int right) {
if (left >= right) return 1; // 只有一个节点或空节点都只能构成一种树
int ans = 0;
for (int i = left; i <= right; i++)
ans += recursionMethod(left, i - 1) * recursionMethod(i + 1, right);
return ans;
}
public:
int numTrees(int n) {
return recursionMethod(1, n);
}
};
2.2.2 记忆(Memorization)
class Solution {
unordered_map<int, int> memorization;
int recursionMethod(int left, int right) {
if (memorization.count(right - left)) return memorization[right - left];
if (left >= right) return 1;
int ans = 0;
for (int i = left; i <= right; i++)
ans += recursionMethod(left, i - 1) * recursionMethod(i + 1, right);
memorization[right - left] = ans;
return ans;
}
public:
int numTrees(int n) {
return recursionMethod(1, n);
}
};
在 2. 暴力破解(Brute Force)一章的分析中,我们得到了第一个重要的关系式:
现在假设G(n)表示从1 ~ n所能形成的BST总数;F(i, n)表示以某个节点(i)为root,在剩下的[1, …, i - 1]和[i + 1, …, n]个节点中所能形成BST总数;那么结合暴力破解的思路,能递推出G(n)和F(i, n)的关系式:
Formula1:
G(n) = F(1, n) + F(2, n) + F(3, n) + … + F(n - 1, n) + F(n, n)
Formula1等价暴力解法的for循环;对于F(i, n),可以利用上图的关系进行分解,也就是递归中的“左节点总数 * 右节点总数”可以等价为:
Formula2:
F(i, n) = G([1, …, i - 1]) * G([i + 1, …, n])
G([1, …, i - 1]) 表示从1 ~ i - 1所能形成的BST总数;同理,G([i + 1, …, n])表示i + 1 ~ n所能形成的BST总数,我们又知道节点所能构成的BST总数与具体的节点无关,而与多少个节点有关,那么求两个G式子的节点数;或者使用G(n)的定义,Formula2可以改写成:
Formula3:
F(i, n) = G(i - 1 - 1 + 1) * G(n - (i + 1) + 1) = G(i - 1) * G(n - i)
根据Formula3,Formula1可以改写成:
Formula4:
G(n) = G(0) * G(n - 1) + G(1) * G(n - 2) + … + G(n - 1) * G(0)
得到Formula4,我们就得到了本题的DP递推公式。现在题目要我们求G(n),又已知G(0) = G(1) = 1,那么我们可求得所有的DP递推公式如下:
G(1) = G(0) * G(0);
G(2) = G(0) * G(1) + G(1) * G(0) ;
G(2) = G(0) * G(2) + G(2) * G(1) + G(2) * G(0)
…
G(n) = G(0) * G(n - 1) + G(1) * G(n - 2) + … + G(n - 1) * G(0)
所以我们从G(1)开始计算,算到G(n)即可得到答案。
class Solution {
public:
int numTrees(int n) {
vector<int> memo(n + 1, 0);
memo[0] = memo[1] = 1;
for (int i = 2; i <= n; i++) // G(1), G(2), G(3) ...... G(n - 1), G(n), Jump G(1) over
for (int j = 1; j <= i; j++) // G(n) = G(0) * G(n - 1) + G(1) * G(n - 2) ...... G(n - 1) * G(0)
memo[i] += memo[j - 1] * memo[i - j]; // F(i, n) = G(i - 1) * G(n - i), 1 <= i <= n
return memo[n];
}
};
这道题还有用数学计算的方法,暂时看不懂,等会面有时间研究明白再更新这个方法,有兴趣的童鞋可以先参考下面两篇文章: