[Leetcode] 1130. Minimum Cost Tree From Leaf Values

1130 Minimum Cost Tree From Leaf Values

综合性比较高的一道题,记录一下,便于思考和回顾。

原题地址: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.


题意:
构造一棵二叉树,树的每个节点的子节点数目为0或2,即没有单子节点的情形。给定的arr为按顺序排列的叶结点,树的非叶节点值等于左右子树中最大值的乘积。要求构建的树所有非叶节点值之和最小,并返回这个和。

1、DP解法
既然给定的arr是顺次排列的叶节点,那么就一定存在着一个分割点,使得数组的左半部分构成左子树,右半部构成右子树。每次分割完将出现一个新的父节点(非叶节点),值为左右子树中最大值的乘积,而最终结果需累加这个新节点的值。我们的目标就是让每次分割后累加的值最小。

定义[i,j]范围内非叶结点累加和为dp[i][j],则递推公式:
d p [ i ] [ j ] = m i n i < = k < j { d p [ i ] [ k ] + d p [ k + 1 ] [ j ] + m a x ( a r r [ i ] , . . . , a r r [ k ] ) ∗ m a x ( a r r [ k + 1 ] , . . . , a r r [ j ] ) } dp[i][j] = min_{i<=k<j}\{dp[i][k] + dp[k+1][j] + max(arr[i],...,arr[k]) * max(arr[k+1],...,arr[j])\} dp[i][j]=mini<=k<j{dp[i][k]+dp[k+1][j]+max(arr[i],...,arr[k])max(arr[k+1],...,arr[j])}

最终结果返回dp[0][n-1]即可。

class Solution {
    public int mctFromLeafValues(int[] arr) {
        int l = arr.length;
        int[][] dp = new int[l][l];
        //避免后续嵌套循环寻找最大值
        int[][] max = new int[l][l];
        for(int i=0;i<l;i++){
            dp[i][i]=0;
            int cur_max = 0;
            for(int j=i;j<l;j++){
                cur_max = Math.max(cur_max, arr[j]);
                max[i][j] = cur_max;
            }
        }
        for(int len=1;len<l;len++)
            for(int left=0;left+len<l;left++){
                int right = left+len;
                dp[left][right] = Integer.MAX_VALUE;
                if(len==1)
                    dp[left][right] = arr[left] * arr[right];
                else{
                    for(int k=left;k<right;k++){
                        dp[left][right] = Math.min(dp[left][k] + dp[k+1][right] + max[left][k]*max[k+1][right], dp[left][right]);
                    }
                }
            }
        return dp[0][l-1];
    }
}

时间复杂度: O ( n 3 ) O(n^3) O(n3)
空间复杂度: O ( n 2 ) O(n^2) O(n2)

2、贪心解法
换一个思路想,从arr中两个相邻的值a和b组成一棵子树,新父节点的值为a*b,假设a<b,那么自此a将不会再被使用,从arr中移除。而b将会被传到更高层,作为新的最大子节点继续运算,在arr中保留。为了让每次叠加的代价a*b最小,选取arr中的最小值a,b取a的两个邻居中较小的那个。重复这个过程,直到数组为空或只剩1个数字。

这个思路一开始让我相当纠结。比较直观的想法是大的值应该尽量贴近root节点,以减少其参与的运算次数,然而上述的贪心过程只是每次选择最小值,如果a的邻居非常大,那不是先把大的值选出来了吗?不就和直观想法相违背?仔细思考了一下,这个贪心过程只是一个运算顺序,而不是建树的顺序,一个值作为b被选取出的次数实际上就代表了它的深度,而我们趋向于选择较小的邻居,因此最大值将被留到最后,在树中的深度也就越小,与直观想法并不冲突。

class Solution {
    public int mctFromLeafValues(int[] arr) {
        int l = arr.length;
        ArrayList<Integer> list = new ArrayList<>();
        for(int a:arr)
            list.add(a);
        int result=0;
        while(list.size()>1){
            int min_value = Integer.MAX_VALUE;
            int min_idx = -1;
            for(int i=0;i<list.size();i++){
                if(list.get(i)<min_value){
                    min_value = list.get(i);
                    min_idx = i;
                }
            }
            if(min_idx > 0 && min_idx < list.size()-1)
                result += min_value * Math.min(list.get(min_idx-1), list.get(min_idx+1));
            else
                result += min_value * ((min_idx==0)?list.get(1):list.get(list.size()-2));
            list.remove(min_idx);
        }
        return result;
    }
}

时间复杂度: O ( n 2 ) O(n^2) O(n2)
空间复杂度: O ( n ) O(n) O(n)

3、贪心优化
上述贪心方法的时间瓶颈是每次remove后都要重新遍历寻找最小元素。前面已经说过了,每次贪心选择并不是建树的顺序,只是为了选取一个被消耗(删除)的点。最小值并不是目的,实际上只要取一个左右邻居都比它大的点,就能达到被删除的目的。

我们可以维护一个递减排列的栈,在遍历arr时,若是递减排列(栈顶元素大于当前值)则入栈,这样栈顶元素就是a的候选,至少能保证左邻居比它小。此时如果右邻居比它大(栈顶元素小于当前值),那么此栈顶元素就可以作为a被选取出来并删除。

class Solution {
    public int mctFromLeafValues(int[] arr) {
        int l = arr.length;
        Stack<Integer> stack = new Stack<>();
        stack.push(Integer.MAX_VALUE);
        int res = 0;
        for(int i=0;i<l;i++){
            while(stack.peek()<=arr[i]){
                int a = stack.pop();
                res += a*Math.min(stack.peek(), arr[i]);
            }
            stack.push(arr[i]);
        }
        while(stack.size()>2)
            res += stack.pop()*stack.peek();
        return res;
    }
}

时间复杂度: O ( n ) O(n) O(n)
空间复杂度(最坏,元素递减排列): O ( n ) O(n) O(n)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值