Problem¶
Given an integer n, return a list of all possible full binary trees with n nodes. Each node of each tree in the answer must have Node.val == 0.
Each element of the answer is the root node of one possible tree. You may return the final list of trees in any order.
A full binary tree is a binary tree where each node has exactly 0 or 2 children.
Example 1:
**Input:** n = 7
**Output:** [[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|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]]
Constraints:
1 <= n <= 20
Solve¶
Over complicating simulation approach¶
hash
There isn’t any thing said we could reused the tree node, which I mean all TreeNode object from Root A should be different from Root B . We assuming that this No reused rule is required.
We try to build up all solution from bottom:
- If
nis even (n % 2 == 0), there are no possible full binary trees since we always need to have an odd number of nodes to form a full binary tree. - If
nis 1, there is only one possible full binary tree with a single node having a value of 0.
For any odd n = 2x + 1, where x is an integer, we can generate the full binary trees from the previous n - 2 = 2x - 1 trees. We use the following building blocks:
self.roots: We keep track of an array of roots math have previousn-2nodes, which starting with a tree containing a single node with value 0 whenn = 1.self.leafs: We also keep track of all leaf nodes in each tree ofself.rootsarray. The purpose of this is to quickly add new nodes directly to the leaf nodes instead of looping through all the nodes to find the leaf nodes.
In the implementation, I using
tupleto store both ofself.rootsandself.leafsin one arrayself.roots
We then follow these steps to build the trees:
- We start with
n = 1, and initializeself.rootswith a tree containing a single node with value 0. Also, we initializeself.leafs[root] = [<all leaf nodes of the root tree>]. - For any
n = 2x + 1(wheren = 3, 5, 7, ...), we try to generate new trees from the previousn - 2trees. To do this efficiently:- We loop through each
curr_rootroot tree inself.rootsand their correspondedself.leafs - (Step 2) Add two leaf nodes to any of this root leaf.
- Use the
copyTreefunction to create a copy of the all tree and then revert the change from step 2. - We then hash the new tree using the
hashNodefunction to check for any collision with previously generated trees. - If there is no collision (the hash is not in
visited), we add the new tree to theresultlist along with its leaf nodes. - Update our
self.rootsto newresult
- We loop through each
- We continue this process until we reach
n. Finally, we return the generated trees inself.roots
The simulation-based approach works, but it is unnecessarily complex and slow due to the copying of trees and hash computations.
class Solution:
def hashNode(self, root, index = 1):
if root is None:
return 0
result = index << 5 + index
if root.left:
result += self.hashNode(root.left, 2*index)
if root.right:
result += self.hashNode(root.right, 2*index + 1)
return result
def copyTree(self, source):
result = None
leafs = []
result = TreeNode(source.val)
if source.left:
result.left, leafsLeft = self.copyTree(source.left)
leafs += leafsLeft
if source.right:
result.right, leafsRight = self.copyTree(source.right)
leafs += leafsRight
if source.left is None and source.right is None:
leafs.append(result)
return result, leafs
def addLeaf(self):
result = []
visited = set()
for root, leafs in self.roots:
for leaf in leafs[::-1]:
leaf.left = TreeNode(0)
leaf.right = TreeNode(0)
new_root, new_leafs = self.copyTree(root)
hashN = self.hashNode(new_root)
if hashN not in visited:
result.append((new_root, new_leafs))
visited.add(hashN)
leaf.left = None
leaf.right = None
return result
def allPossibleFBT(self, n: int) -> List[Optional[TreeNode]]:
if n%2 == 0:
return []
self.roots = []
for i in range(1, n+1, 2):
tmp = []
if i == 1:
root = TreeNode(0)
tmp = [(root, [root])]
self.roots = tmp
continue
tmp = self.addLeaf()
self.roots = tmp
return [root for root, _ in self.roots]
Minimal¶
The simulation approach can be greatly simplified using dynamic programming and memorization to avoid redundant computations.
We use the following steps to find all possible full binary trees with n nodes:
- If
nis even (n % 2 == 0), there are no possible full binary trees, so we return an empty list. - If
nis 1, there is only one possible full binary tree with a single node having a value of 0. We return this tree as a list containing a single node. - For any odd
n = 2x + 1(wherexis an integer), we recursively find all possible full binary trees for each possible left subtree with nodesi(whereiranges from 1 ton - 1with a step of 2), and the right subtree with nodesn - i - 1. We then combine these left and right subtrees with a root having a value of 0, and add them to the result list. - We use memorization to store the results of subproblems in the
cachedictionary to avoid redundant computations.
The minimal approach is much more efficient and straightforward, and it accurately finds all possible full binary trees with n nodes, each having a root node with a value of 0. It returns the final list of trees in any order.
class Solution:
def allPossibleFBT(self, n: int, cache = None) -> List[TreeNode]:
if cache is None:
cache = {}
if n in cache:
return cache[n]
if n % 2 == 0:
return []
if n == 1:
cache[n] = [TreeNode()]
return [TreeNode()]
res = []
for i in range(1, n, 2):
left = self.allPossibleFBT(i, cache)
right = self.allPossibleFBT(n - i - 1, cache)
for l in left:
for r in right:
root = TreeNode(0, l, r)
res.append(root)
cache[n] = res
return res
Created : August 16, 2023
