7

【LeetCode动态规划#04】不同的二叉搜索树(找规律,有点像智力题) - dayceng

 1 year ago
source link: https://www.cnblogs.com/DAYceng/p/17255860.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

不同的二叉搜索树

力扣题目链接(opens new window)

给定一个整数 n,求以 1 ... n 为节点组成的二叉搜索树有多少种?

img

先找一下关系

当n = 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:真难啊动态规划


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK