96.不同的二叉搜索树—动态规划

96.不同的二叉搜索树—动态规划

难度 中等

题目

给你一个整数 n ,求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。

示例1

3个节点的二叉搜索树种数

输入:n = 3
输出:5

示例2

输入:n = 1
输出:1

解题思路

在解题前,我们要知道什么是二叉搜索树,它或者是一棵空树,或者是具有下列性质的二叉树: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉搜索树。注意:它的左右子树也是二叉搜索树。这样的话,就有重叠子问题了,我们可以用动态规划来解决此问题。在此之前先对动态规划做个介绍。


动态规划

相信很多同学都对动态规划算法有所耳闻,应该也见过很多题是用动态规划算法来解题的,但很多人对动态规划还是很懵,遇到后感觉不知道如何去下手,或者根本就不知道这道题可以用动态规划来解决。
首先这篇文章以这道二叉搜索树提出动态规划算法,我会介绍一下从哪些方面去考虑这个算法,然后会再加两道入门题来便于大家理解。

  • 什么是动态规划
    动态规划(Dynamic Programming),简称DP,如果某一问题有很多重叠子问题,使用动态规划是最有效的。动态规划中每一个状态一定是由上一个状态推导出来的,也就是当前状态会用到上个状态的信息

  • 动态规划的思路步骤(五步)

    1. dp[]数组及其下标的含义(一般情况下,问题求什么,含义就是什么)
    2. 状态转移方程(找规律,找当前状态和上个(或上几个)状态的关系,找当前状态如何用到以前状态的信息)
    3. dp[]初始化(根据题目来进行dp[0]或dp[1]等的初始化,一般初始为0或1)
    4. 遍历顺序(for 循环)
    5. 打印dp[]数组(调试检查问题可用)

根据上面动态规划的思路步骤,我们来解决二叉搜索树这道题。题目问由n个节点组成的二叉搜索树有多少种?那么我们的dp[i]数组的含义就表示由i个节点组成的二叉搜索树的种类数。接下来,我们以示例1为例,分析一下:
3个节点组成的二叉搜索树情况

  • 左子树节点数为0,右子树节点数为2
  • 左子树节点数为1,右子树节点数为1
  • 左子树节点数为2,右子树节点数为0

则3个节点组成的二叉搜索树种类为以上三种情况之和,每种情况的左右子树构成一个大的整体二叉树,为相乘关系,左右子树都是二叉搜索树;故左子树节点数为0,即为dp[0],右子树节点数为2,即为dp[2],故dp[3]=dp[2]*dp[0]+dp[1]*dp[1]+dp[0]*dp[2]
类推,可得到动态转移方程为dp[i] += dp[j]*dp[i-j-1](i=1,2,…,n; j=0,1,…,i-1)。
初始化dp[0]应该为1,否则若为0的话,上面乘积就为0了。
对于遍历顺序(for循环),在上面转移方程后已经推出了i,j的范围,即为for循环

代码
class Solution(object):
    def numTrees(self, n):
        """
        :type n: int
        :rtype: int
        """
        """动归五步曲:
            1.dp[]数组及下标i含义
            2.状态转移方程
            3.dp初始化
            4.遍历顺序(for循环)
            5.打印dp数组
        """
        dp = [0] * (n+1)
        dp[0] = 1
        for i in range(1, n+1):
        	for j in range(i):
        		dp[i] += dp[j] * dp[i-j-1]
        return dp[-1]
        

为了加深大家对动态规划的理解,我们再来看两道动归入门题。
首先第一道:509.斐波那契数

斐波那契数,通常用 F(n) 表示,形成的序列称为斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:
F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1
给你 n ,请计算 F(n) 。

解题思路
  • 题目求什么,dp[]数组含义就是什么,那么此题dp[i]就表示第i项的斐波那契数值
  • 递推公式题目也给了我们,dp[i] = dp[i-1] + dp[i-2],即dp[i] = dp[j] + dp[j-1](i=2,3,…,n; j=i-2, i-1)
  • 初始化题目也给了。dp[0] = 0, dp[1] = 1
  • 遍历顺序由i, j的范围也得到了
代码
class Solution(object):
    def fib(self, n):
        """
        :type n: int
        :rtype: int
        """
        if n < 2:
        	return n
        dp = [0] * (n+1)
        dp[0], dp[1] = 0, 1
        for i in range(2, n+1):
        	for j in range(i-2, i):
        		dp[i] = dp[j] + dp[j-1]
        return dp[-1]
        

通过这道题,大家是不是稍微更加理解了动态规划呢?趁热打铁,我们再来一道:70.爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。

解题思路

此题比上一题难度稍微增加一点,难点在于动归方程,不慌,我们来按照动归解题步骤一点一点来分析

  • 题目问什么,dp[]数组含义就是什么,则dp[i]就表示i阶楼梯爬到楼顶的方法数
  • i阶楼梯,怎么用到前面的状态信息,看题,每次可以爬1或2个台阶,那么,i阶楼梯方法就是i-1阶楼梯方法(再爬1阶到顶)加上i-2阶楼梯方法(再爬2阶到顶);也就是说,上一个状态要么爬1个台阶到当前状态,要么爬2个台阶到当前状态,则有dp[i]=dp[i-1] + dp[i-2],即dp[i]=dp[j] + dp[j-1](i=2,3,…,n; j=i-2,i-1)
  • 初始化当0个或1个台阶时,方法都为1,即dp[0]=1, dp[1]=1
  • for循环由上面推导动归方程也出来了
代码
class Solution(object):
    def climbStairs(self, n):
        """
        :type n: int
        :rtype: int
        """
        dp = [0] * (n+1)
        dp[0], dp[1] = 1, 1
        for i in range(2, n+1):
        	for j in range(i-2, i):
        		dp[i] = dp[j] + dp[j-1]
        return dp[-1]
        
总结

以后遇到动态规划类题就按照这五步来分析,我想一定是没有问题的,这里,我没有打印dp[]数组,当我们写完程序出现问题时,可以打印dp[]数组来进行调试,看看打印的数组是不是正确的,如果打印出来有问题,那么可能就是动归方程或者初始化或者遍历顺序出现了问题,到这里,关于动归就讲结束了,大家好好加油,如果有更好的方法,欢迎留言下方评论o( ❛ᴗ❛ )o!

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值