今天是LeetCode专题第62篇文章,我们一起来聊聊LeetCode的96题,Unique Binary Search Trees(唯一的二叉搜索树)。
这道题的官方难度是Medium,点赞3576,反对132,从这个点赞比上来看,这道题的质量还是很高的。也可以算得上是特别好评。通过率52.8%,从通过率上来看在Medium当中算是偏高的,实际上也的确如此,这道题没有什么诡计,也没有很刁钻的内容,考验的完全是我们对于数据结构最基本的理解。
和上题的内容很相似,如果有没有看过上题的同学可以点击传送门回顾一下:
给定一个整数n,代表1-n这n个整数。要求用这n个数生成二叉搜索树(BST)。请问可以构成多少种结构不同的BST?
Input: 3
Output: 5
Explanation:
Given n = 3, there are a total of 5 unique BST's:
1 3 3 2 1
\ / / / \ \
3 2 1 1 3 2
/ / \ \
2 1 2 3
某种程度上来说这是上一题的简化版,我们当然可以复用上一题的代码,但这没有任何意义。如果只是复用上一题的代码是无法通过这题了,原因也很简单,因为n的范围变大了。
在上一题当中我们用递归的思路求解出了所有构建树的可能性,在这一题当中我们没必要完整地构建树了,只需要计算种类就可以了。我们虽然不能直接复用上一题的代码,但是思路却是可以借鉴的,我们一样可以采用分治和递归的思路。
在分治法当中我们把一个比较复杂的大问题转化成若干个相对容易解决的小问题,比如著名的归并排序就是利用的这个思路。而递归做的是什么事情呢,是函数自己调用自己,解决一个本质相同只是规模更小的问题。这两者并不是等价的,分治法并不一定需要递归,递归也不一定就是用来实现分治法。准确得说分治法是一种算法,而递归是一种解决问题的思想是算法的实现方式。这两者高度相关但是并不相同,这道题是这两者一个巧妙的结合。
我们回到问题本身,我们假设有一个函数f,可以求出包含n个大小不同的数的BST的数量。那么显然f(n)就是答案。所以我们要做的就是想办法得到这个f。但是也很明显,这个f是很难或者是没法直接得到的。针对这种情况我们需要对算法找一个开头,再构建出一种嵌套方法。这一点有些类似于数学归纳法,我们先证明n=1时成立,再证明如果n=k时成立可以证明得到n=k+1也成立,那么我们就可以循环嵌套得到所有的证明。
递归也是一样,我们先得到n=1时的解,再通过n=k得到n=k+1,那么我们就可以递归得到所有的解。所以问题的核心就是怎么构建出这个循环嵌套的过程。这一点需要我们对问题进行深入分析。
对于有n个元素的BST来说,它的根节点的组成有n种可能。假设根节点是i,那么我们可以得到它的左子树包含1-i-1这i-1个元素,右子树包含i+1到n这n-i个元素。那么以i为根节点的BST的数量就是,而根节点一共有n种可能,所以。
那么很明显,代码呼之欲出。
class Solution:
def numTrees(self, n: int) -> int:
def dfs(n):
if n <= 1:
return 1
ret = 0
for i in range(1, n+1):
ret += dfs(i-1) * dfs(n-i)
return ret
return dfs(n)
是不是很简单呢?但是很遗憾这段代码是无法通过的,原因也很简单,因为递归浪费了很多时间。我们在计算dfs(n)的时候,它将dfs(0)到dfs(n-1)都计算了一遍。而我们计算dfs(n+1)的时候,这些值又会重复再计算一遍。这样层层累加之下,会有很多的值是重复计算的,这会带来大量的消耗,是毫无意义的。
我们可以做一个很简单的优化,就是将所有dfs的值都存储起来。如果我们要求的dfs(n)之前已经计算过了,那么直接返回不在重复计算了。
class Solution:
def numTrees(self, n: int) -> int:
record = {}
record[0] = 1
record[1] = 1
def dfs(n):
if n in record:
return record[n]
ret = 0
for i in range(1, n+1):
ret += dfs(i-1) * dfs(n-i)
return ret
return dfs(n)
这种很简单的优化方法叫做记忆化搜索,说白了就是将可能会出现重复计算的中间值存储起来防止重复计算,从而优化运行速度。
如果我们换一种写法写成递推的形式,那么就成了动态规划了。所以某种程度上来说,动态规划和记忆化搜索是同一种算法,只是表现形式不同。所以很多课本上先从记忆化搜索开始介绍动态规划,就是这么个道理。
class Solution:
def numTrees(self, n: int) -> int:
dp = [0 for _ in range(n+2)]
dp[0] = 1
dp[1] = 1
for i in range(2, n+1):
for j in range(1, i+1):
dp[i] += dp[j-1] * dp[i-j]
return dp[n]
- END -