Reference: LeetCode
Difficulty: Medium
My Post: [Java] Recursion & Iteration Solutions (DP, Clone, Memoization)
Problem
Given an integer $n$, generate all structurally unique BST’s (binary search trees) that store values
1 ... n
.
Example:
1 | Input: 3 |
Analysis
Recursion
If we directly apply the solution in 96. Unique Binary Search Trees
, we have an incorrect result. Consider the following example.
1 | n = 5, i = 3 |
As you can see, although there are still 2 nodes in the right subtrees, the values are not the same. In the previous approach, it did not handle this situation.
So rather than passing a parameter n
(number of nodes), we now pass down two parameters lo
and hi
indicating the range of values of a tree.
For example, generateTrees(1, 5)
generate trees whose values range from 1
to 5
, and each of them has a chance to be the root, which also includes the information of number of nodes n
. Say when the root is 3
, we calculate generateTrees(1, 2)
and generateTrees(4, 5)
.
Note: When n == 0
, it returns []
instead of [[null]]
.
1 | public List<TreeNode> generateTrees(int n) { |
Time: It is at most bounded by $O(N \times N!)$. A tighter bound would be Catalan number times $N$ since we’ve done $N$ times, which is $N \times G_N = O(N\times\frac{4^N}{N^{3/2}}) = O(\frac{4^N}{N^{1/2}})$.
Space: $O(N \times G_N) = O(\frac{4^N}{N^{1/2}})$
DP (Clone)
Reference: link
Let’s denote tree[i]
as the list of trees of size i
. Think of tree[i]
as our building blocks for a larger tree.
1 | Example: n = 3 |
So we can compute all possible trees for tree[1]
, tree[2]
, …, then we can construct tree[n]
by previous results.
For a small n = 3
, we notice that when we calculate tree[2]
we want all possible combinations for tree[2]
([1 2]
, [2 3]
). Furthermore, if we have a large n = 100
, we want all the combinations as follows [1 2]
, [2 3]
, [3 4]
, …, [99 100]
(each of them has two structures).
Since these trees have the same two types of structures:
1 | x y |
We can actually construct all the trees by [1 2]
plus some constant, say offset
. For example, [5 6]
can be constructed as follows:
1 | 1 +4 5 |
Say the problem is n = 100
. During the execution of the algorithm when i = 98
, we want to get all possible trees for i = 98
as the root. The size of the left subtree is 97
and the subtree is picked from tree[97]
; the size of the right subtree is 2
and the subtree is picked from tree[2]
.
For the left subtree, we already have tree[97]
computed as [1 2 3 ... 97]
.
For the right subtree, we want [99 100]
, which can be computed by [1 2]
plus offset = 98
.
1 | 1 +98 99 |
Therefore, given a tree root
, we can generate a new tree by cloning with an offset
.
1 | // adding offset to each node of a tree root |
For input n
, the result we want is tree[n]
([1 2 3 ... n]
). Here is the code for generateTrees(n)
:
1 | public List<TreeNode> generateTrees(int n) { |
Time: N/A (I don’t know T_T)
Space: N/A (I don’t know T_T)
DP (2D)
Judge: 1ms
, faster than 99.95%
. I have nothing to say.
Here is the code I wrote:
1 | public List<TreeNode> generateTrees(int n) { |
Time: N/A (I don’t know T_T)
Space: N/A (I don’t know T_T)