当青训营遇上码上掘金
题目描述
- 现有 n 个宽度为 1 的柱子,给出 n 个非负整数依次表示柱子的高度,排列后如下图所示,此时均匀从上空向下撒青豆,计算按此排列的柱子能接住多少青豆。(不考虑边角堆积)
以下为上图例子的解析
输入:height = [5,0,2,1,4,0,1,0,3]
输出:17
解析:上面是由数组 [5,0,2,1,4,0,1,0,3] 表示的柱子高度,在这种情况下,可以接 17 个单位的青豆。
题目分析
本题“攒青豆”由LeetCode上题目“接雨水”改编而来,大致解法相同,可参考“接雨水”的实现思路进行解题。可行的解题方法分别有动态规划法、单调栈法、双指针法。其中,动态规划法是最经典的解法。参考接雨水思路,我将逐步从暴力求解出发,通过“空间换时间”进行优化,逐步实现动态规划法,从而使读者理解动态规划的本质,掌握动态规划的解题思路。
求解流程
思路
给定柱子的高度数组,对于下标为的柱子,下雨后水能到达的最大高度等于下标左右两边的所有柱子最大高度的最小值,下标处能接的雨水量等于下标处的水能到达的最大高度减去。 公式表示为:
暴力求解
对于中的每一个元素,对其左右两边进行扫描寻找最大值,使用不同的语言会有不同的代码实现方法,如Python语言有封装好的函数,可以实现数组切片、求解数组内最大值等方法。本文使用Java语言,自行实现给定下标范围的数组内最大值。
给定数组、开始下标、结束下标,搜索范围为左闭右闭区间,时间复杂度为。
public int getMax(int[] a, int start, int end){
// 左闭右闭
int maxValue = Integer.MIN_VALUE;
for(int i = start; i <= end; i ++){
maxValue = max(maxValue,a[i]);
}
return maxValue;
}
初始化可以接到的青豆为0,遍历数组中的每个元素,根据公式计算下标处可以接到的青豆数并累计。
int res = 0;
for(int i = 1; i < height.length - 1;i ++){
// 需要把i算上,从左或从右计算最大值,否则产生负数
res += min(getMax(height,0,i),getMax(height,i, height.length-1)) - height[i];
}
总体时间复杂度为。
空间换时间
上述做法的时间复杂度较高是因为需要对每个下标位置都向两边扫描。如果已经知道每个位置两边的最大高度,则可以在的时间复杂度实现获得青豆总量。 基于动态规划思想,预先创建两个数组和,存储每个位置左右两边的最大高度。公式表示为:
同理可得。
int[] leftMax = new int[height.length];
leftMax[0] = height[0];
for(int i = 1; i < height.length; i ++){
leftMax[i] = max(leftMax[i - 1],height[i]);
}
int[] rightMax = new int[height.length];
rightMax[height.length - 1] = height[height.length - 1];
for(int i = height.length - 2; i >= 0 ; i --){
rightMax[i] = max(rightMax[i + 1],height[i]);
}
动态规划
得到数组和的每个元素值之后,下标处能接的雨水量表示为:
遍历每个下标位置即可得到能接的雨水总量。
int res = 0;
for(int i = 0;i < height.length; i ++){
res += min(leftMax[i], rightMax[i]) - height[i];
}