题目:
解题思路
-
阅读左神算法书动态规划章节“机器人行走的步数”一题有感,我们对这类问题可以从暴力解法逐步找到动态规划的解法
-
首先,序列1,2,…,n是一个升序序列,可以认为是一棵搜索二叉树的中序序列,因此:
- 我们迭代的把每个数当作是根节点,求它的左右子树分别有几种组合,相乘即可,比如N为5,根节点为3时,求左子树[1,2],右子树[4,5]的组合数相乘,叠加到最后结果
- 当然如果一个子树为空,当前组合数就是另一棵子树的组合数
- 这是一个递归过程,写出如下代码:
def numTreesEnum(self, n: int) -> int: def process(start, end): # 如果当前树为空 if start >= end: return 1 res = 0 for i in range(start, end+1): res += process(start, i-1) * process(i+1, end) return res return process(1, n)
-
以上的代码时间复杂度是O(n!), 因为对子问题作了很多重复计算:当N为5时,假设根节点为1,我们的计算里包含了子树[3,4,5],根节点为2时,计算里也包含了子树[3,4,5]…
-
经过分析我们发现,上述算法的过程是以“哪个数是根节点”作为迭代基础,求算节点数量为end-start+1的子树长度并作累积,我们不妨以“根节点”,“树的节点数量”两个变量为研究对象,构造出一个dp矩阵,
-
那么当n=4时,如果行对应“根节点”,列对应“树的节点数量”,这个矩阵就是这样的:
-
如果我们求出了最后一列的值,这一列求和就是最终答案!
-
Base Case是什么?
-
当节点数量为1时,根节点也为1,只有1种情况,填入1;根节点为2,3,4时,这些树不存在,所以都是0,所以矩阵变成这样:
-
然后再来看节点数量为2的时候:
- 根节点为1时,左子树0个节点,右子树1个节点,共1种情况;
- 根节点为2时,右子树0个节点,左子树1个节点,共1种情况;
- 其他情况都是0
-
然后再来看节点数量为3的时候:
- 根节点为1时,左子树0个节点,右子树2个节点(因为sum(因为第2列的和是2),共2种情况;
- 根节点为2时,左子树1个节点(因为第1列的和是1),右子树1个节点(因为第1列的和是1),共1种情况;
- 根节点为3时,左子树2个节点(因为第2列的和是2),右子树0个节点,共2种情况
-
由上面分析得出,对每一种情况,只需求出左子树数量L,右子树数量R,使当前情况对应的格子等于第L-1列的和乘上第R-1列的和即可
-
最后的矩阵:
-
我们对每一列的计算,只需要用到之前列的和,所以我们不需要nxn的dp矩阵,只需要长度为n的dp数组,
-
最后算法时间复杂度是O(n^2), 空间复杂度是O(n),代码如下:
代码
class Solution:
def numTrees(self, n: int) -> int:
res = [0] * (n + 1)
res[1] = 1
for j in range(2, n+1):
curr_sum = 0
for i in range(1, n+1):
if i > j:
break
left = i - 1
right = j - i
left = 1 if left == 0 else left
right = 1 if right == 0 else right
curr_sum += res[left] * res[right]
res[j] = curr_sum
return res[n]