96.不同的二叉搜索树—动态规划
难度 中等
题目
给你一个整数 n ,求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。
示例1
输入:n = 3
输出:5
示例2
输入:n = 1
输出:1
解题思路
在解题前,我们要知道什么是二叉搜索树,它或者是一棵空树,或者是具有下列性质的二叉树: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉搜索树。注意:它的左右子树也是二叉搜索树。这样的话,就有重叠子问题了,我们可以用动态规划来解决此问题。在此之前先对动态规划做个介绍。
动态规划
相信很多同学都对动态规划算法有所耳闻,应该也见过很多题是用动态规划算法来解题的,但很多人对动态规划还是很懵,遇到后感觉不知道如何去下手,或者根本就不知道这道题可以用动态规划来解决。
首先这篇文章以这道二叉搜索树提出动态规划算法,我会介绍一下从哪些方面去考虑这个算法,然后会再加两道入门题来便于大家理解。
-
什么是动态规划
动态规划(Dynamic Programming),简称DP,如果某一问题有很多重叠子问题,使用动态规划是最有效的。动态规划中每一个状态一定是由上一个状态推导出来的,也就是当前状态会用到上个状态的信息。 -
动态规划的思路步骤(五步)
- dp[]数组及其下标的含义(一般情况下,问题求什么,含义就是什么)
- 状态转移方程(找规律,找当前状态和上个(或上几个)状态的关系,找当前状态如何用到以前状态的信息)
- dp[]初始化(根据题目来进行dp[0]或dp[1]等的初始化,一般初始为0或1)
- 遍历顺序(for 循环)
- 打印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!