LeetCode - Medium - 1130. Minimum Cost Tree From Leaf Values

Topic

  • Dynamic Programming
  • Stack
  • Tree

Description

https://leetcode.com/problems/minimum-cost-tree-from-leaf-values/

Given an array arr of positive integers, consider all binary trees such that:

  • Each node has either 0 or 2 children;
  • The values of arr correspond to the values of each leaf in an in-order traversal of the tree. (Recall that a node is a leaf if and only if it has 0 children.)
  • 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.

Example 1:

Input: arr = [6,2,4]
Output: 32
Explanation:
There are two possible trees.  The first has non-leaf node sum 36, and the second has non-leaf node sum 32.

    24            24
   /  \          /  \
  12   4        6    8
 /  \               / \
6    2             2   4

Constraints:

  • 2 < = a r r . l e n g t h < = 40 2 <= arr.length <= 40 2<=arr.length<=40
  • 1 < = a r r [ i ] < = 15 1 <= arr[i] <= 15 1<=arr[i]<=15
  • It is guaranteed that the answer fits into a 32-bit signed integer (ie. it is less than 2 31 2^{31} 231).

Analysis

方法一:动态规划DP

时杂度: O ( n 3 ) O(n^3) O(n3)

空杂度: O ( n 2 ) O(n^2) O(n2)

从下到上

动规五步诗

  1. DP数组意
  2. 归纳递推式
  3. 初始元素值
  4. 定序来遍历
  5. 最后举个例

一、DP数组意

d p [ i ] [ j ] dp[i][j] dp[i][j]为数组a下标i到j的元素值化成叶子节点值,叶子节点组合成最小花销树的值。 i < = j i <= j i<=j

二、归纳递推式

通过几个简单案例,可归纳出递推式。

d p [ i ] [ j ] = m i n ( d p [ i ] [ k ] + m a x ( a [ i ] , a [ i + 1 ] , . . . , a [ k ] ) ∗ m a x ( a [ k + 1 ] , a [ k + 2 ] , . . . , a [ j ] ) + d p [ k + 1 ] [ j ] , . . . ) dp[i][j] = min(dp[i][k] + max(a[i],a[i + 1],...,a[k]) * max(a[k+1], a[k + 2],...,a[j])+dp[k+1][j],...) dp[i][j]=min(dp[i][k]+max(a[i],a[i+1],...,a[k])max(a[k+1],a[k+2],...,a[j])+dp[k+1][j],...)

( i < = k < = j ) (i<=k<=j) (i<=k<=j)

三、初始元素值

如果i与j相等,则 d p [ i ] [ j ] = 0 dp[i][j]=0 dp[i][j]=0

为了方便理解,先初始化两个相邻数组元素的乘积, d p [ 0 ] [ 1 ] = a [ 0 ] ∗ a [ 1 ] , d p [ 1 ] [ 2 ] = a [ 1 ] ∗ a [ 2 ] . . . dp[0][1]=a[0]*a[1],dp[1][2]=a[1]*a[2]... dp[0][1]=a[0]a[1],dp[1][2]=a[1]a[2]...,这步可省略,取而代之用递推式求出。

四、定序来遍历

将dp数组形象成一个正方形,遍历方向从左上顶角斜到右下顶角,然后回到左上顶角右侧一点到右下顶角上测一点,周而复始,到右上顶角为止。

五、最后举个例

例如,数组为 a = { 6 , 2 , 4 } a=\{6,2,4\} a={6,2,4},求最小销树的值。

d p [ 0 ] [ 0 ] = d p [ 1 ] [ 1 ] = d p [ 2 ] [ 2 ] = 0 dp[0][0]=dp[1][1]=dp[2][2]=0 dp[0][0]=dp[1][1]=dp[2][2]=0

d p [ 0 ] [ 1 ] = a [ 0 ] ∗ a [ 1 ] = 12 dp[0][1]=a[0]*a[1]=12 dp[0][1]=a[0]a[1]=12

d p [ 1 ] [ 2 ] = a [ 1 ] ∗ a [ 2 ] = 8 dp[1][2]=a[1]*a[2]=8 dp[1][2]=a[1]a[2]=8

d p [ 0 ] [ 2 ] = m i n ( d p [ 0 ] [ 0 ] + m a x ( a [ 0 ] ) ∗ m a x ( a [ 1 ] , a [ 2 ] ) + d p [ 1 ] [ 2 ] , d p [ 0 ] [ 1 ] + m a x ( a [ 0 ] , a [ 1 ] ) ∗ m a x ( a [ 2 ] ) + d p [ 2 ] [ 2 ] ) dp[0][2] = min(dp[0][0]+max(a[0])*max(a[1],a[2])+dp[1][2],dp[0][1]+max(a[0],a[1])*max(a[2])+dp[2][2]) dp[0][2]=min(dp[0][0]+max(a[0])max(a[1],a[2])+dp[1][2],dp[0][1]+max(a[0],a[1])max(a[2])+dp[2][2])
= m i n ( 0 + 6 ∗ 4 + 8 , 12 + 6 ∗ 4 + 0 ) =min(0+6*4+8,12+6*4+0) =min(0+64+8,12+64+0)
= m i n ( 32 , 36 ) = 32 =min(32,36)=32 =min(32,36)=32

012
001232
1-08
2--0

最后,数组为 a = { 6 , 2 , 4 } a=\{6,2,4\} a={6,2,4},最小花销树的值为 d p [ 0 ] [ 2 ] = 32 dp[0][2]=32 dp[0][2]=32

从上到下

计算 d p [ i ] [ j ] dp[i][j] dp[i][j]时,先查找数组dp是否已经计算过了。

如果计算过了,就返回对应数组元素值。如果没有,就用递推式向下递归求出。

PS:个人觉得从下到上高效些。因为从上到下方式,到最后还是从下而上返回值。

方法二:贪心算法

时杂度: O ( n ) O(n) O(n)

空杂度: O ( n ) O(n) O(n)

Above approach is kind of like brute force since we calculate and compare the results all possible pivots.
To achieve a better time complexity, one important observation is that when we build each level of the binary tree, it is the max left leaf node and max right lead node that are being used, so we would like to put big leaf nodes close to the root. Otherwise, taking the leaf node with max value in the array as an example, if its level is deep, for each level above it, its value will be used to calculate the non-leaf node value, which will result in a big total sum.

With above observation, the greedy approach is to find the smallest value in the array, use it and its smaller neighbor to build a non-leaf node, then we can safely delete it from the array since it has a smaller value than its neightbor so it will never be used again. Repeat this process until there is only one node left in the array (which means we cannot build a new level any more)
link

方法三:单调栈 Monotonic stack

时杂度: O ( n ) O(n) O(n)

空杂度: O ( n ) O(n) O(n)

In the greedy approach of 2), every time we delete the current minimum value, we need to start over and find the next smallest value again, so repeated operations are more or less involved.
To further accelerate it, one observation is that for each leaf node in the array, when it becomes the minimum value in the remaining array, its left and right neighbors will be the first bigger value in the original array to its left and right. This observation is a clue of a possible monotonic stack solution as follows.
link

A little dry run here:

arr =[6,2,4]

numwhile()stack.peek() <= nummidstackres = mid * num
6stack=[]--[6]-
2stack=[6]=6 , 6 > 2-[6,2]-
4stack=[6,2]=2, 2 < 42[6,4]res += 2 * 4 = 8

Stack = [6,4]
stack.size() == 2 > 1
stack.pop() = 4
stack.peek() = 6
res += stack.pop() * stack.peek()
8 + 24 = 32

link

如数组 a = { 15 , 13 , 5 , 3 , 15 } a=\{15,13,5,3,15\} a={15,13,5,3,15},最小花销树的图如下:

结合此图,理解算法

Submission

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

public class MinimumCostTreeFromLeafValues {

	// 方法一:我自己写的,DP,从下而上
	public int mctFromLeafValues(int[] arr) {
		int dp[][] = new int[arr.length][arr.length];

		// 初始化元素值
		// 其实这段可省略,合并到下面一段循环。如果要省略,将下面stepWidth初始化为1
		for (int i = 0; i + 1 < arr.length; i++) {
			dp[i][i + 1] = arr[i] * arr[i + 1];
		}

		for (int stepWidth = 2; stepWidth < arr.length; stepWidth++) {
			for (int i = 0; i + stepWidth < arr.length; i++) {
				int min = Integer.MAX_VALUE, end = i + stepWidth;
				for (int j = i; j < end; j++) {// j 将数组分成两段
					int temp = dp[i][j] + max(arr, i, j) * max(arr, j + 1, end) //
							+ dp[j + 1][end];
					if (temp < min)
						min = temp;
				}
				dp[i][end] = min;
			}
		}

		return dp[0][arr.length - 1];
	}

	private int max(int[] array, int start, int end) {
		int max = array[start];
		for (int i = start + 1; i <= end; i++)
			if (array[i] > max)
				max = array[i];
		return max;
	}

	// 方法一之二:别人写的,DP,从上而下
	public int mctFromLeafValues2(int[] arr) {
		int n = arr.length;
		int[][] dp = new int[n][n];
		return dfs(arr, 0, n - 1, dp);
	}

	public int dfs(int[] arr, int s, int e, int[][] dp) {
		if (s == e)
			return 0;
		if (dp[s][e] > 0)
			return dp[s][e];
		int ans = Integer.MAX_VALUE;
		for (int i = s; i < e; i++) {
			int left = dfs(arr, s, i, dp);
			int right = dfs(arr, i + 1, e, dp);
			int maxLeft = 0, maxRight = 0;
			for (int j = s; j <= i; j++)
				maxLeft = Math.max(maxLeft, arr[j]);
			for (int j = i + 1; j <= e; j++)
				maxRight = Math.max(maxRight, arr[j]);
			ans = Math.min(ans, left + right + maxLeft * maxRight);
		}
		dp[s][e] = ans;
		return ans;
	}

	// 方法二:贪心算法
	public int mctFromLeafValues3(int arr[]) {
		List<Integer> A = new ArrayList<>();
		for (int d : arr)
			A.add(d);

		int res = 0;
		while (A.size() != 1) {
			int minIndex = A.indexOf(Collections.min(A));

			if (minIndex > 0 && minIndex < A.size() - 1)
				res += A.get(minIndex) * Math.min(A.get(minIndex - 1), A.get(minIndex + 1));

			else if (minIndex == 0)
				res += A.get(minIndex) * A.get(minIndex + 1);
			else
				res += A.get(minIndex) * A.get(minIndex - 1);

			A.remove(minIndex);
		}
		return res;
	}

	// 方法三:单调栈
	public int mctFromLeafValues4(int[] arr) {
		if (arr == null || arr.length < 2) {
			return 0;
		}

		int res = 0;
		LinkedList<Integer> stack = new LinkedList<>();
		for (int num : arr) {

			// while num is bigger than peek(), pop and calculate
			while (!stack.isEmpty() && stack.peek() <= num) {
				int mid = stack.pop();
				if (stack.isEmpty())
					res += mid * num;
				else
					res += mid * Math.min(stack.peek(), num);
			}

			stack.push(num); // if num is smaller, push into stack
		}

		// if there are values left in the stack, they sure will be mutiplied anyway
		// and added to the result.
		while (stack.size() > 1) { // > 1 because we have a peek() after pop() below
			res += stack.pop() * stack.peek();
		}

		return res;
	}

}

Test

import static org.junit.Assert.*;


import org.junit.Test;

public class MinimumCostTreeFromLeafValuesTest {

    @Test
    public void test() {
        MinimumCostTreeFromLeafValues mObj = new MinimumCostTreeFromLeafValues();

        assertEquals(32, mObj.mctFromLeafValues(new int[] {6, 2, 4}));
        assertEquals(284, mObj.mctFromLeafValues(new int[] {7, 12, 8, 10}));
        assertEquals(500, mObj.mctFromLeafValues(new int[] {15, 13, 5, 3, 15}));
    }
    
    @Test
    public void test2() {
    	MinimumCostTreeFromLeafValues mObj = new MinimumCostTreeFromLeafValues();
    	
    	assertEquals(32, mObj.mctFromLeafValues2(new int[] {6, 2, 4}));
    	assertEquals(284, mObj.mctFromLeafValues2(new int[] {7, 12, 8, 10}));
    	assertEquals(500, mObj.mctFromLeafValues2(new int[] {15, 13, 5, 3, 15}));
    }
    
    @Test
    public void test3() {
    	MinimumCostTreeFromLeafValues mObj = new MinimumCostTreeFromLeafValues();
    	
    	assertEquals(32, mObj.mctFromLeafValues3(new int[] {6, 2, 4}));
    	assertEquals(284, mObj.mctFromLeafValues3(new int[] {7, 12, 8, 10}));
    	assertEquals(500, mObj.mctFromLeafValues3(new int[] {15, 13, 5, 3, 15}));
    }
    
    @Test
    public void test4() {
    	MinimumCostTreeFromLeafValues mObj = new MinimumCostTreeFromLeafValues();
    	
    	assertEquals(32, mObj.mctFromLeafValues4(new int[] {6, 2, 4}));
    	assertEquals(284, mObj.mctFromLeafValues4(new int[] {7, 12, 8, 10}));
    	assertEquals(500, mObj.mctFromLeafValues4(new int[] {15, 13, 5, 3, 15}));
    }
    
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值