Problem
Given an array arr
of positive integers, consider all binary trees such that:
- Each node has either
0
or2
children; - The values of
arr
correspond to the values of each leaf in an in-order traversal of the tree. - The value of each non-leaf node is equal to the product of the largest leaf value in its left and right subtree, respectively.
Among all possible binary trees considered, return the smallest possible sum of the values of each non-leaf node. It is guaranteed this sum fits into a 32-bit integer.
A node is a leaf if and only if it has zero children.
Examples
Example 1:
graph TD; A(24) --- B(12) & C(4) B --- D(6) & E(2) L(24) --- M(6) & N(8) N --- O(2) & P(4)
|
|
Example 2:
graph TD; A(44) --- B(4) & C(11)
|
|
|
|
Solution
Method 1 - Recursion
- The goal is to optimally convert the array into a binary tree.
- The optimal way to divide the array into left and right subtrees is unknown.
- Notice that dividing the array creates two subarrays, resulting in two subproblems.
- Therefore, we should try all possible ways to divide the array and recursively apply the same logic to the subarrays. Use a 2D array for memoization.
Code
|
|
|
|
Complexity
- ⏰ Time complexity:
O(2^n)
, wheren
is the length of the array. Without memoization, the function will solve each subproblem many times, leading to an exponential time complexity due to the recursive nature of the solution. - 🧺 Space complexity:
O(n)
for the recursion stack, wheren
is the length of the array.
Method 2 - Top Down DP with memoization
We will use dynamic programming to solve the problem. The dynamic programming formula is:
|
|
This means that the smallest possible sum of the values of non-leaf nodes for the subarray from i
to j
can be calculated by dividing the array at every possible k
, where k
is between i
and j
, and combining the results from the left and right subarrays.
Code
|
|
|
|
Complexity
- ⏰ Time complexity:
O(n^3)
, wheren
is the length of the array. This is because we need to fill thedp
array which hasO(n^2)
entries, and for each entry, we can perform up toO(n)
operations. - 🧺 Space complexity:
O(n^2)
for thedp
array used for memoization.
Method 3 - Greedy
Here is the approach:
- The initial solution tries every way to divide the array, but is there truly no better way to find the optimal division?
- We do have some insights. In the final binary tree, non-leaf node values come from the maximum leaf values in the left and right subtrees.
- If the maximum value in the array is at the deepest level, it will be used multiple times for several parent nodes, which is not efficient.
- Thus, we should place the smallest product pair from the array at the deepest level. The smallest product pair is
arr[i] * arr[i + 1]
where the product is the smallest. - After processing the pair, the smaller value will no longer be used and can be discarded.
- The
O(n^2)
solution involves iterating through the array to find the indexi
wherearr[i] * arr[i + 1]
is the smallest, add it to the result, and discard the smaller of the pair. Repeat until the array size is 1.
Code
|
|
|
|
Complexity
- ⏰ Time complexity:
O(n^2)
, wheren
is the length of the array. This is because in the worst case scenario, we need to go through the arrayn
times, and in each pass, we search for the smallest product pair inO(n)
time. - 🧺 Space complexity:
O(n)
, since we use a new array to hold the reduced elements each time we remove a pair.
Method 4 - Monotonic Decreasing Stack
When building a node in the tree, we compare two numbers, a
and b
. The smaller one is removed and won’t be used again, while the larger one remains.
The problem can be translated as:
Given an array A
, choose two neighboring elements a
and b
. Remove the smaller one (min(a, b)
) and the cost is a * b
. What is the minimum cost to reduce the array to one element?
To remove a number a
, it requires a cost of a * b
, where b >= a
. Thus, a
must be removed by a larger number. We aim to minimize this cost, so b
must be minimized.
b
has two possibilities: the first larger number on the left and the first larger number on the right.
The cost to remove a
is a * min(left, right)
.
Algorithm
So, here is the algorithm:
- The second solution removes the smaller value of pair
arr[i]
andarr[i + 1]
with the smallest product in each iteration. - Each iteration actually removes a local minimum value:
- For elements
arr[i - 1]
,arr[i]
, andarr[i + 1]
wherearr[i]
is the local minimum. - The product added to the final result is
arr[i] * min(arr[i - 1], arr[i + 1])
.
- For elements
- The problem translates into removing all local minimum values while finding the first larger element on the left and right.
Similar to Trapping Rain Water OR Largest Rectangle in Histogram.
Use a stack to maintain a decreasing order. When a larger value arr[i]
is encountered, pop from the stack, calculate the product mid * min(arr[i], stack.peek())
, and store it.
If we look at the number array closely, in order to obtain the smallest sum of all non-leaf values, we should merge the smallest values first, i.e. smaller values should be lower leaves to minimize multiplication as the tree grows.
Ex: arr = [4, 3, 2, 1, 5]
There are various ways to construct a tree following the problem’s requirements. To achieve the smallest sum, we should merge 2
and 1
first since they are the smallest values. We use a stack in decreasing order for this. After merging 2
and 1
, the resulting parent node value is 2
. This parent node should ideally have the smallest possible parent as well. We observe that the smallest candidate next to 2
is 3
, making 3
the left child and 1 * 2 = 2
the right child.
|
|
If we observe closely, 3 2 1
forms a decreasing sequence. So, whenever we encounter a “dip” in the array, we calculate the multiplication. For instance, at arr[i]
with the condition arr[i-1] <= arr[i] <= arr[i+1]
, the minimum multiplication is arr[i] * min(arr[i-1], arr[i+1])
. In the example above, this translates to arr[i] = 1, arr[i-1] = 2, arr[i+1] = 5
.
Code
|
|
|
|
Complexity
- ⏰ Time complexity:
O(n)
, wheren
is the length of the array. Each element is pushed and popped from the stack at most once. - 🧺 Space complexity:
O(n)
, for the stack used to store elements.