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
n
is 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
n
is 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-2
nodes, 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.roots
array. 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
tuple
to store both ofself.roots
andself.leafs
in one arrayself.roots
We then follow these steps to build the trees:
- We start with
n = 1
, and initializeself.roots
with 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 - 2
trees. To do this efficiently:- We loop through each
curr_root
root tree inself.roots
and their correspondedself.leafs
- (Step 2) Add two leaf nodes to any of this root leaf.
- Use the
copyTree
function to create a copy of the all tree and then revert the change from step 2. - We then hash the new tree using the
hashNode
function 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 theresult
list along with its leaf nodes. - Update our
self.roots
to 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
n
is even (n % 2 == 0
), there are no possible full binary trees, so we return an empty list. - If
n
is 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
(wherex
is an integer), we recursively find all possible full binary trees for each possible left subtree with nodesi
(wherei
ranges from 1 ton - 1
with 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
cache
dictionary 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