【LeetCode动态规划#04】不同的二叉搜索树(找规律,有点像智力题)
不同的二叉搜索树
给定一个整数 n,求以 1 ... n 为节点组成的二叉搜索树有多少种?
示例:
思路
题意分析
先找一下关系
当n = 1时,如果元素就是1,以1为头节点
1
当n = 2时,分别以1和2为头节点
1 2
/ \
2 1
然后当n = 3时的情况就是示例中给的那几种
找找有什么规律
当n = 3且使用1为头节点时,其右子树的布局和n = 2时的布局是一样的(注意看1-2、2-1和3-2、2-3的方向,是不是一样的,数值不同没有影响)
当n = 3且使用2为头节点时,其左右子树布局和n = 1时的布局是一样的(n = 1是左右子树为空,也算1种情况)
当n = 3且使用3为头节点时,其左子树的布局和n = 2时的布局是一样的
某种程度上,n = 3的二叉搜索树种类情况可以由n = 2以及n = 1推导出来
因此,n = 3时,二叉搜索树种类 = 头节点为1时的情况+头节点为2时的情况+头节点为3时的情况,来组成
即,头1+头2+头3
公式描述
接下来分析不同头节点时的情况
从图中可以看出,头节点为1时有:
1 1
\ \
3 2
/ \
2 3
头节点为1时有多少种二叉搜索树可以用以下公式描述:
头1 = 左子树有0个节点时有几种二叉搜索树 * 右子树有2个节点时有几种二叉搜索树;(2种)
如何理解?
头节点为1来构建二叉搜索树的话,如果左子树只给0个节点,那么左子树的类型就只有1种(也就是空);然后给右子树2个节点的话,那么右子树就可以有3-2和2-3两种情况。
左右子树的情况组合在一起就得到以下结论:
当n = 3时,使用1作为头节点可以构建2种(1*2)不同的二叉搜索树
还不理解再举个例子:
10 / \ 5个节点 10个节点
上述以10(数值无所谓)为头节点的二叉树,其左右子节点的情况如上
那么,可以构成的二叉树的种类一共是:5*10种
同理可以得到头2、头3的公式描述:
头2 = 左子树有1个节点时有几种二叉搜索树 * 右子树有1个节点时有几种二叉搜索树;(1种)
头3 = 左子树有2个节点时有几种二叉搜索树 * 右子树有0个节点时有几种二叉搜索树;(2种)
与示例对照的话发现是可以对上的
还是先来五部曲吧
五步走
1、确定dp数组的含义
根据题目所求可以得到
dp[i]:输入为i时有dp[i]种不同的二叉搜索树
2、确定递推公式
由前面的分析可以知道,当输入n为3时,可以组成的二叉搜索树种类(也就是dp[3])是有分别使用1、2、3作为头节点时产生的种类相加得到的。
即,
dp[3] = 头1+头2+头3
dp[3] = dp[0]dp[2] + dp[1]dp[1] + dp[2]dp[0];
明确了上述问题后可以开始讨论dp[i]
dp[i]可以从哪里求出来?
那肯定是由以1、2、3...i为头节点的所有情况相加得出,于是我们需要枚举所有头节点情况
以 j 来代表头节点数,那么该二叉搜索树的左子树有多少个节点?答案是 j-1 个
(举个例子来理解:示例中以3为头节点时,其左子树是不是有两个节点)
那么该二叉搜索树的右子树有多少个节点?答案是 i-j 个
因为我们这里是二叉搜索树,现在以i为头节点了,右子树的节点值一定都比 i 大,总节点数是 i ,那么留给右子树的节点数就是i-j了
套用dp数组的定义:
输入为 j-1 时有 dp[j-1] 种不同的二叉搜索树;
输入为 i-j 时有 dp[i-j] 种不同的二叉搜索树;
那么dp[i]怎么求?
根据 公式描述 中的讨论可得:dp[i] = dp[j-1] * dp[i-j];
(只是当前j下的dp[i])
因为 j 是代表遍历所有头节点的情况,所以要把头节点为1~i的情况都相加才能得出最后的dp[i]
即递推公式应该是:dp[i] += dp[j-1] * dp[i-j];
(i个节点有多少种不同的二叉搜索树)
怎么理解?
拿前面的例子dp[3]来说
dp[3] = dp[0]dp[2] + dp[1]dp[1] + dp[2]dp[0];
此处j的遍历范围是1~i,可以写成以下形式
dp[3] = dp[1-1]dp[3-1] + dp[2-1]dp[3-2] + dp[3-1]dp[3-3];
3、确定初始化方式
前面的讨论也说了dp[0]、dp[1] (即,n=0、n=1)可以用于推出后续情况
那么就要对这两者进行初始化吗?其实只需要初始化dp[0]就行了,dp[1]也可以通过dp[0]推出
dp[0]的含义是什么?输入的i是0,0个节点有多少种不同的二叉搜索树呢?答案是1个,因为空二叉树也是一种二叉搜索树
所以dp[0] = 1;
并且这也符合递推公式的要求,因为如果有空节点,其种类如果是0,那么不论后面的其他子树有几种情况,结果都是0,就没有意义了,因此空节点的种类应该是1(如果左右子树都空的话,也就是1*1=1,递推还能继续进行下去)
4、确定遍历顺序
还是从dp[3]来看
dp[3] = dp[0]dp[2] + dp[1]dp[1] + dp[2]dp[0];
dp[3]都是由小于3的状态累加推导来的,所以就要从小到大遍历,才可以利用之前遍历的状态
for(int i = 1; i <= n; ++i){
for(int j = 1; j <= i; ++j){
dp[i] += dp[j-1] * dp[i-j];
}
}
代码
class Solution {
public:
int numTrees(int n) {
//定义dp数组
vector<int> dp(n + 1);
//初始化dp数组
dp[0] = 1;
//dp[1] = 1;//不用初始化dp[1],否则按理来说dp[1]应该是1,手动初始化会使其变为2
//遍历
for(int i = 1; i <= n; ++i){//此处i确实要取3,所以有等于号
for(int j = 1; j <= i; ++j){
dp[i] += dp[j - 1] * dp[i - j];
}
}
// cout << dp[1]<< endl;//打印dp数组debug
return dp[n];//确实要返回n的dp而不是n-1
}
};
ps:真难啊动态规划