1. 题目
现有 n 个宽度为 1 的柱子,给出 n 个非负整数依次表示柱子的高度,排列后如下图所示,此时均匀从上空向下撒青豆,计算按此排列的柱子能接住多少青豆。(不考虑边角堆积)
💡 以下为上图例子的解析:
输入:height = [5,0,2,1,4,0,1,0,3] 输出:17
解析:上面是由数组 [5,0,2,1,4,0,1,0,3] 表示的柱子高度,在这种情况下,可以接 17个单位的青豆。
2. 思路
这道题目原题为著名题目42. 接雨水,本题是攒青豆,一共有几种解题方法。参考接雨水思路,我将总结2种较为常见的解法:按列解与动态规划。
法1:按列算
这是最直观也是最容易想到的方法,后面的动态规划方法也是基于本方法优化,因此需要掌握。我们遍历数组height的所有元素,按照列来算。主要思路如下:
对于每一列,我们都求当前列左右最高边界,然后进行判断:
- 如果左右列都找到大于当前列的高度,那么:计算当前列高度与最小值的高度差,并加入总数
- 如果存在一边小于等于当前列高度,则说明会朝至少一遍漏水,因此:我们需要跳过当前列
- 时间复杂度:O(n^2)
代码
class Solution {
public int trap(int[] height) {
int count = 0;
for (int i = 0; i < height.length; i++) {
int left = height[i];
int right = height[i];
// 找到当前列左右最高的 且 比当前列高的index作为边界
for (int j = i; j >= 0; j--) {
left = Math.max(left, height[j]);
}
for (int j = i; j < height.length; j++) {
right = Math.max(right, height[j]);
}
// 如果左右列有一边最高等于当前列,说明装不了水
if (left == height[i] || right == height[i]) continue;
// 算当前列能装水的单位数
count += Math.min(left, right) - height[i];
}
return count;
}
}
法2:动态规划
由于需要对每列寻找左边与右边的最大高度,法1是在遍历的时候同时寻找,因此时间复杂度是O(n^2)。但是我们可以在遍历之前用2个数组保存每个列的左右最大高度,从而可以把时间复杂度优化到O(n)。主要思路总结如下:
- 数组保存第i个位置的左右墙最高值
- 从头到尾遍历,按列求每列的雨水值
- 状态转移方程:当前第i列左右墙最高值 = Max(第i-1列最高值, 当前列墙高)
不过我们需要注意dp数组的定义,不同定义后面遍历的条件处理有些许区别。这里我们把leftMax[i]数组定义成:在第i列中左侧(包括自身)的最大高度,leftMax[i]同理。
代码1:dp最大高度包括当前列,因此最小高度=当前列
class Solution {
public int trap(int[] height) {
// 打表,首先记录当前列的左右最大高度
// dp[i]数组意义:在第i列中左/右边最大高度(包括自身)
int[] leftMax = new int[height.length];
int[] rightMax = new int[height.length];
// 保存第i个元素的左侧最高的墙高
leftMax[0] = height[0];
for (int i = 1; i < height.length; i++) {
// 状态转移方程:比较前面列的最高和当前列谁高
leftMax[i] = Math.max(leftMax[i - 1], height[i]);
}
// 保存第i个元素的右侧最高的墙高
rightMax[height.length - 1] = height[height.length - 1];
for (int i = height.length - 2; i >= 0; i--) {
rightMax[i] = Math.max(rightMax[i + 1], height[i]);
}
int count = 0;
for (int i = 0; i < height.length; i++) {
// 如果和当前列相等,说明当前列高度最大,回漏水,直接跳过
if (leftMax[i] == height[i] || rightMax[i] == height[i]) continue;
count += Math.min(leftMax[i], rightMax[i]) - height[i];
}
return count;
}
}
代码2:dp最大高度只看前一列,因此遍历需要判断是否≤当前列
class Solution {
public int trap(int[] height) {
//时间复杂度:O(n),空间复杂度:O(n)
int[] leftHighest = new int[height.length];
int[] rightHighest = new int[height.length];
// 初始值,可以省略
// leftHighest[0] = 0;
// rightHighest[0] = 0;
// 保存第i个元素的左侧最高的墙高
for (int i = 1; i < leftHighest.length; i++) {
leftHighest[i] = Math.max(leftHighest[i - 1], height[i - 1]);
}
// 保存第i个元素的右侧最高的墙高
for (int i = rightHighest.length - 2; i >= 0; i--) {
rightHighest[i] = Math.max(rightHighest[i + 1], height[i + 1]);
}
int sum = 0;
for (int i = 0; i < height.length; i++) {
// 需要跳过小于等于当前列的情况
int diff = Math.min(leftHighest[i], rightHighest[i]) - height[i];
if (diff > 0) sum += diff;
}
return sum;
}
}