(多种写法)【LeetCode96】不同的二叉搜索树

本文最早发自微信公众号【有趣理工男】,朋友们可以关注一波。

题目

Given an integer n, return the number of structurally unique BST’s (binary search trees) which has exactly n nodes of unique values from 1 to n.

示例1

img

输入:n = 3
输出:5

示例 2:

输入:n = 1
输出:1

思路

解决这道题我们先从基本的 Top-down (递归)写法入手,之后改写成 Bottom-up 写法。之所以这样,是因为 Top-down 的写法更加容易理解。

首先,从题目开始分析。给定一个数字 n ,求从节点值 1n 互不相同的二叉搜索树有多少个。显然,我们可以将这个大问题拆分为小问题求解。即求根节点值为 k 的二叉搜索树有多少个,再把 k=1k=2,… ,k=n 相加,即可求出答案。

如上面示例1,我们要求 n=3 ,只需要分别求当根节点为 123 时的二叉搜索树有多少个,再将它们相加,就可以得到答案。

上面我们拆分了问题,得到了子问题。现在我们对子问题求解。

在这里插入图片描述

设给定整数 n ,拆分为 n 个子问题求解。

我们定义一个函数 G[n] 来表示长度为 n 的序列能构成的不同BST的个数。

每一个子问题根节点值为 k ,此时左子树的节点个数为 k-1 ,而右子树节点个数为 n-k

再定义一个函数 numTrees(k, n) 来表示根节点为 k 、序列长度为 n 的BST的个数。(注意这个函数与前面的 G[n] 不一样,这个是子问题的求解函数)

对于上图,当 k=1 时(根节点为1),左子树可以组成的BST的个数为 G[0] ,右子树可以组成的BST的个数为 G[4]

1️⃣我们有:

G [ 5 ] = n u m T r e e s ( 1 , 5 ) + n u m T r e e s ( 2 , 5 ) + n u m T r e e s ( 3 , 5 ) + n u m T r e e s ( 4 , 5 ) + n u m T r e e s ( 5 , 5 ) G[5] = numTrees(1, 5) + numTrees(2, 5) + numTrees(3, 5) + numTrees(4, 5) + numTrees(5, 5) G[5]=numTrees(1,5)+numTrees(2,5)+numTrees(3,5)+numTrees(4,5)+numTrees(5,5)

2️⃣进一步我们有:
n u m T r e e s ( 1 , 5 ) = G [ 0 ] ∗ G [ 4 ] n u m T r e e s ( 2 , 5 ) = G [ 1 ] ∗ G [ 3 ] n u m T r e e s ( 3 , 5 ) = G [ 2 ] ∗ G [ 2 ] n u m T r e e s ( 4 , 5 ) = G [ 3 ] ∗ G [ 1 ] n u m T r e e s ( 5 , 5 ) = G [ 4 ] ∗ G [ 0 ] numTrees(1, 5) = G[0] * G[4]\\ numTrees(2, 5) = G[1] * G[3]\\ numTrees(3, 5) = G[2] * G[2]\\ numTrees(4, 5) = G[3] * G[1]\\ numTrees(5, 5) = G[4] * G[0] numTrees(1,5)=G[0]G[4]numTrees(2,5)=G[1]G[3]numTrees(3,5)=G[2]G[2]numTrees(4,5)=G[3]G[1]numTrees(5,5)=G[4]G[0]

对于1️⃣,整理为一般形式:
G ( n ) = ∑ k = 1 n n u m T r e e s ( k , n ) G(n) = \sum_{k=1}^{n}numTrees(k, n) G(n)=k=1nnumTrees(k,n)
对于2️⃣,有人可能会有疑惑:为什么是相乘?

这里我借LeetCode官方题解中的一副图:

fig1

相当于左子树有 G(i-1) 种组合,右子树有 G(n-i) 种组合。要求解整体能有多少种组合,则用熟悉来排列组合来做,直接把左子树的组合数量乘以右子树的组合数量,即可求解。

我们则可以求出 2️⃣ 的一般形式:
n u m T r e e s ( k , n ) = G ( k − 1 ) ∗ G ( n − k ) numTrees(k, n) = G(k-1)*G(n-k) numTrees(k,n)=G(k1)G(nk)
注意:k 是根节点的值。

把上述的两个公式写在一起:
G ( n ) = ∑ k = 1 n n u m T r e e s ( k , n ) n u m T r e e s ( k , n ) = G ( k − 1 ) ∗ G ( n − k ) G(n) = \sum_{k=1}^{n}numTrees(k, n)\\ numTrees(k, n) = G(k-1)*G(n-k) G(n)=k=1nnumTrees(k,n)numTrees(k,n)=G(k1)G(nk)
结合成一个式子得:
G ( n ) = ∑ k = 1 n G ( k − 1 ) ∗ G ( n − k ) G(n)=\sum_{k=1}^{n}G(k-1)*G(n-k) G(n)=k=1nG(k1)G(nk)

最后,我们确定一下终止条件(Base case)。

即当 n=0n=1 时,有: G(0) = 1 和 G(1) = 1 。其意义为:空树的BST个数为1。只有一个节点组成的BST个数为1 。

Top-Down Solution

根据前面的分析,从递归式很容易写出递归的代码。

def numTrees(n):
    if n == 0 or n == 1:
        return 1
    res = 0
    for k in range(1, n+1):
        res += numTrees(k-1) * numTrees(n-k)
    return res

但是,上面的代码放在 LeetCode 会超时。怎么办呢?首先,我们找出影响代码速度的地方,很明显是因为 numTrees() 多次的计算,那么有没有一种方法可以将之前计算出函数的结果存下来呢?有的。我们可以用下述两种方法来优化:

  • 字典
  • lru_cache

字典优化

def numTrees(n):
    cache = dict()

    def helper(n):
        if n in cache:
            return cache[n]
        if n == 0 or n == 1:
            return 1
        res = 0
        for k in range(1, n+1):
            res += helper(k-1) * helper(n-k)
        cache[n] = res
        return res

    return helper(n)

在这里插入图片描述

lru_cache 缓存优化

from functools import lru_cache

@lru_cache()
def numTrees(n):
    if n == 0 or n == 1:
        return 1
    res = 0
    for k in range(1, n+1):
        res += numTrees(k-1) * numTrees(n-k)
    return res

在这里插入图片描述

Bottom-Up Solution

def numTrees(n):
    G = [0] * (n+1)
    G[0] = 1
    G[1] = 1
    for i in range(2, n+1):
        for k in range(1, n+1):
            G[i] += G[k-1] * G[i-k]
    return G[n]

最后这是 Bottom-Up的写法,其中 G 是一个列表。例如,G[2] 存了节点数量为 2 能构成不同BST的个数;G[n] 存了节点数量为 n 能构成不同BST的个数。由于 G[0]G[1] 为 Base case,所以外层循环 i 取值从 2 开始,直到 n 。每层内层循环取值 k 相当于把 k 作为二叉树的根节点,显然 k 的取值是从 1 到 n 的。

注意,我们每次内层循环是用 += ,理由前面有讲到,此处不再赘述。

G[k-1]k-1 是左子树的个数,根据下图也很好理解。但是为什么 G[i-k] 中是 i-k 而不是 n-i 呢?

fig1

理解很简单,假设 n=3 ,因为我们现在是动态规划,动态地求 G[2]G[3] 。使用 G[n-k] 的话,每次内层循环都是 G[3-k] 了,这样是错误的。正确使用 G[i-k] 每次内层循环乘号右边应该是 G[2-k]G[3-k]

在双层循环之后,切记要返回 G[n]

附上提交结果:

在这里插入图片描述

总结

正所谓,动态规划,层层相扣。将大问题分解为子问题对分析问题很有帮助,而从子问题找出解决方法是本题核心。想不到题目解的时候,不妨将几种情况写出,寻找它们规律。

本文最早发自微信公众号【有趣理工男】

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值