叶值的最小代价生成树【LC1130】
给你一个正整数数组 arr
,考虑所有满足以下条件的二叉树:
- 每个节点都有
0
个或是2
个子节点。 - 数组
arr
中的值与树的中序遍历中每个叶节点的值一一对应。 - 每个非叶节点的值等于其左子树和右子树中叶节点的最大值的乘积。
在所有这样的二叉树中,返回每个非叶节点的值的最小可能总和。这个和的值是一个 32 位整数。
如果一个节点有 0 个子节点,那么该节点为叶节点
2023/5/31 贪心更好理解
递归+记忆化
- 思路:
生成的节点的值取决于左右子树叶子节点的最大值的乘积,而数组arr
的值与树的中序遍历中每个叶子节点的值一一对应,因此我们可以将数组划分为左右两个非空子数组,分别对应树的左右子树,递归地求解每个子树的所有非叶节点的值的最小可能总和。 - 子问题:每颗子树中所有非叶节点的值的最小可能总和等于其左右子树中非叶节点的值的最小可能总和,大区间由小区间转移得到,本题实际为区间dp
- 定义dfs(i,j),表示某子树包含叶子节点为数组
arr
中下标范围[i,j]时,其所有非叶子节点的值的最小可能和,那么最终答案为dfs(0,n−1) - 递归过程:枚举每个分割点k,该子树的最小成本为左右子树的成本之和+父节点值(左右子树叶子节点最大值乘积),枚举取最小值即可dfs(i,j)=mini≤k<j[dfs(i,k)+dfs(k+1,j)+max(arr[i,k])∗max(arr[k+1,j])]
- 递归边界:i==j,只有一个元素时,返回0
- 实现
class Solution { private Integer[][] f; private int[][] g; public int mctFromLeafValues(int[] arr) { int n = arr.length; f = new Integer[n][n]; g = new int[n][n]; for (int i = n - 1; i >= 0; --i) { g[i][i] = arr[i]; for (int j = i + 1; j < n; ++j) { g[i][j] = Math.max(g[i][j - 1], arr[j]); } } return dfs(0, n - 1); } private int dfs(int i, int j) { if (i == j) { return 0; } if (f[i][j] != null) { return f[i][j]; } int ans = 1 << 30; for (int k = i; k < j; k++) { ans = Math.min(ans, dfs(i, k) + dfs(k + 1, j) + g[i][k] * g[k + 1][j]); } return f[i][j] = ans; } } 作者:ylb 链接:https://leetcode.cn/problems/minimum-cost-tree-from-leaf-values/solutions/2290549/python3javacgotypescript-yi-ti-shuang-ji-qpwv/ 来源:力扣(LeetCode) 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
复杂度
- 时间复杂度:O(n3)
- 空间复杂度:O(n2)
递推
- 实现
class Solution { public int mctFromLeafValues(int[] arr) { int n = arr.length; int[][] f = new int[n][n]; int[][] g = new int[n][n]; for (int i = n - 1; i >= 0; --i) { g[i][i] = arr[i]; for (int j = i + 1; j < n; ++j) { g[i][j] = Math.max(g[i][j - 1], arr[j]); f[i][j] = 1 << 30; for (int k = i; k < j; ++k) { f[i][j] = Math.min(f[i][j], f[i][k] + f[k + 1][j] + g[i][k] * g[k + 1][j]); } } } return f[0][n - 1]; } } 作者:ylb 链接:https://leetcode.cn/problems/minimum-cost-tree-from-leaf-values/solutions/2290549/python3javacgotypescript-yi-ti-shuang-ji-qpwv/ 来源:力扣(LeetCode) 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
复杂度
- 时间复杂度:O(n2)
- 空间复杂度:O(n2)
贪心
- 思路:模拟+贪心
根据题意,从叶子结点生成非叶子结点的过程其实就是每次从给定数组arr
的相邻元素取一对来组合,n个元素变成n−1个元素,直至只剩一个元素位置。
- 那么朴素的dfs可以遍历所有选取可能,从
arr
里面枚举每一对相邻的组合,然后保留两者里面较大的(因为生成非叶子节点的值为左右子树叶子节点最大值的乘积),arr
元素减少一个,继续取。 - 我们还可以使用贪心优化:为了使生成叶子节点的值【全局最优】尽可能小,我们每次因选择相邻元素乘积最小的一组进行合并【局部最优】
- 贪心实现
class Solution { public int mctFromLeafValues(int[] arr) { // 两两组合(相乘)数组中的元素,返回最小的组合后的乘积之和 List<Integer> list = new ArrayList<>(); for (int num : arr){ list.add(num); } return dfs(list); } public int dfs(List<Integer> arr){ int n = arr.size(); if (n == 1){// 不需要合并 return 0; } List<Integer> temp = new ArrayList<>(); // 找到最小的组合 int index = 0, min = arr.get(0) * arr.get(1); for (int i = 1; i < n - 1; i++){ if (arr.get(i) * arr.get(i + 1) < min){ index = i; min = arr.get(i) * arr.get(i + 1); } } // 生成新的arr for (int i = 0; i < index; i++){ temp.add(arr.get(i)); } temp.add(Math.max(arr.get(index), arr.get(index + 1))); for (int i = index + 2; i < n; i++){ temp.add(arr.get(i)); } return min + dfs(temp); } }
复杂度
- 时间复杂度:O(n2)
- 空间复杂度:O(n)
- 优化实现:合并n-1次,合并时int数组每次向前移动一位
class Solution { public int mctFromLeafValues(int[] arr) { int sum = 0; int len = arr.length; //我们要抛弃len-1个值,每次循环抛弃一个值 for(int i = 0;i<len-1;i++){ int index = 0; int min = arr[0]*arr[1]; //模拟,将相邻乘积最小的index记录下来。len-1-i后的值都是抛弃的值,不用统计 for(int j = 0;j<len-1-i;j++){ if(arr[j]*arr[j+1]<min){ index = j; min = arr[j]*arr[j+1]; } } sum+=arr[index]*arr[index+1]; //统计结果 arr[index]= Math.max(arr[index],arr[index+1]); //将两个值的最大值覆盖给index //更新index后面的值,将所有值向前移动一格,达到抛弃一个值的目的 for(int j = index+1;j<len-1-i;j++){ arr[j] = arr[j+1]; } } return sum; } }
复杂度
- 时间复杂度:O(n2)
- 空间复杂度:O(1)