所有可能的真二叉树

标签: 递归 记忆化搜索 动态规划 二叉树

难度: Medium

给你一个整数 n ,请你找出所有可能含 n 个节点的 真二叉树 ,并以列表形式返回。答案中每棵树的每个节点都必须符合 Node.val == 0

答案的每个元素都是一棵真二叉树的根节点。你可以按 任意顺序 返回最终的真二叉树列表

真二叉树 是一类二叉树,树中每个节点恰好有 02 个子节点。

示例 1:

输入:n = 7
输出:[[0,0,0,null,null,0,0,null,null,0,0],[0,0,0,null,null,0,0,0,0],[0,0,0,0,0,0,0],[0,0,0,0,0,null,null,null,null,0,0],[0,0,0,0,0,null,null,0,0]]

示例 2:

输入:n = 3
输出:[[0,0,0]]

提示:

  • 1 <= n <= 20

Submission

运行时间: 50 ms

内存: 19.6 MB

# https://space.bilibili.com/206214
f = [[] for _ in range(21)]
f[1] = [TreeNode(0)]
for i in range(3, 21, 2):
    f[i] = [TreeNode(0, left, right)
            for left_size in range(1, i, 2)
            for left, right in product(f[left_size], f[i - 1 - left_size])]

class Solution:
    def allPossibleFBT(self, n: int) -> List[Optional[TreeNode]]:
        return f[n]

Explain

这个题解使用了动态规划的思想。首先,它初始化了一个列表 `f`,其中 `f[i]` 用于存储所有包含 `i` 个节点的真二叉树的根节点。由于真二叉树的节点数必须是奇数(除了根节点外,每两个子节点加一个节点),因此只需要计算奇数个节点的情形。对于每个奇数 `i`,遍历所有可能的左子树大小 `left_size`(也必须是奇数),然后对于每个 `left_size`,通过 `product` 函数从 `f[left_size]` 和 `f[i - 1 - left_size]` 中取出所有可能的左、右子树组合,为每对左右子树创建一个新的根节点,最后将这些树根节点存储在 `f[i]` 中。`product` 是笛卡尔积,用于生成所有可能的左右子树组合。

时间复杂度: O(4^n / n^(3/2))

空间复杂度: O(4^n / n^(3/2))

# https://space.bilibili.com/206214
f = [[] for _ in range(21)] # 初始化存储各个节点数的真二叉树列表
f[1] = [TreeNode(0)] # 只有一个节点时,只有一棵树: 只有根节点
for i in range(3, 21, 2): # 只考虑奇数个节点的情况
    f[i] = [TreeNode(0, left, right) # 为每种左右子树组合创建新的树根节点
            for left_size in range(1, i, 2) # 遍历所有可能的左子树大小
            for left, right in product(f[left_size], f[i - 1 - left_size])] # 从可能的左右子树中生成所有组合

class Solution:
    def allPossibleFBT(self, n: int) -> List[Optional[TreeNode]]: # 返回包含n个节点的所有可能的真二叉树
        return f[n] # 直接返回预计算结果

Explore

在解决'所有可能的真二叉树'问题时,动态规划是一个合适的选择,因为这个问题具有明显的最优子结构特性。每棵真二叉树都可以通过选择左右子树的不同组合来构建,而这些子树自身也是真二叉树。动态规划允许我们存储和重用较小子问题的解(即较小数量节点的所有可能真二叉树形态),从而避免了重复计算,并大幅提高效率。如果使用纯递归或其他方法,每次需要构建同样大小的真二叉树时都需重新计算,效率会明显降低。

在此题解中,使用`product`函数确实是为了生成所有可能的左右子树组合,但这并不意味着每次都会重新计算所有子树组合。动态规划的实现方式通过缓存机制存储了每个节点数下所有可能的真二叉树(在列表`f`中)。因此,当使用`product`函数组合左右子树时,它实际上是从已经计算并存储的结果中提取组合,而不是重新计算每棵子树。这极大地减少了计算量并提高了效率。

在题解中,`f[i]`的初始化从i=3开始,并且只考虑奇数个节点,是因为根据真二叉树的定义,除了根节点外,每个节点都必须有两个子节点。这意味着任何真二叉树的节点数必须是奇数。例如,一个节点的树是真二叉树(只有根),三个节点的树可以是根节点下挂两个子节点,但两个节点的树不能满足每个非根节点都有两个子节点的条件,因此不是真二叉树。

尽管题解中使用了递归的方法,但由于我们利用了动态规划存储了中间结果,实际上降低了递归的需求,因为大部分时间我们是在读取预先计算的结果而非发起新的递归调用。这极大地减少了递归深度。对于极大规模数据,如果递归深度仍然是问题,可以考虑使用迭代的方式重写算法,或者在语言支持的情况下增加递归调用栈大小。