「这是我参与2022首次更文挑战的第18天,活动详情查看:2022首次更文挑战」。
题目:给定一个数组,数组中元素的大小代表宽为1的柱子高度,求由这些柱子堆成的形状能接多少雨水。
解题思路
直观的想法类似水桶原理,水桶能装的水取决于水桶边低的一面,本题可以看成一个大水桶中多个水桶的累加,每个数字可以看作是一个水桶,对于每个水桶,我们找到水桶的左边界和右边界,之后水桶中的水取决于边界较小的一方,如果当前水桶的数值大于等于左边界,则表示当前水桶装不了水,如果小于左边界且属于左右两个边界的较小值,则当前水桶可装水为边界值减去当前水桶高,若右边界较小,则同理。需要注意的是:此处计算左右边界时需要考虑自身,这样即使边界不满足也不会出现负数(只会出现0的情况)。并且数组的第一个数字和最后一个数字必定是不考虑的,此时这两个数为一个大水桶的两个边界。代码如下:
public static int trap(int[] height) {
int maxWater = 0;
int len = height.length;
for(int i=1;i<len-1;i++){
int maxLeft=0, maxRight=0;
for(int m=i;m>=0;m--){
maxLeft = Math.max(maxLeft, height[m]);
}
for(int n=i;n<len;n++){
maxRight = Math.max(maxRight, height[n]);
}
maxWater += Math.min(maxLeft, maxRight) - height[i];
}
return maxWater;
}
上述代码有两层循环,因此时间复杂度为,仅使用常数量的参数,空间复杂度为。
动态规划优化
上述方法中对每次遍历的元素都重新循环寻找左边界和右边界,导致时间复杂度变大,实际上,我们可以事先计算好每个元素的左边界和右边界,此处计算的方法和题解中分享的类似,但存在一点不同,题解中都是不考虑当前元素本身,而我的方法是考虑本身,这样在后面就不需要进行值大小的判断,代码如下:
public int trap(int[] height) {
int maxWater = 0;
int len = height.length;
int[] maxLeft = new int[len];
int[] maxRight = new int[len];
for(int i=0;i<len;i++){
if(i>0) {
maxLeft[i] = Math.max(maxLeft[i - 1], height[i]);
}else {
maxLeft[i] = height[i];
}
}
for(int j=len-1;j>=0;j--){
if(j<len-1) {
maxRight[j] = Math.max(maxRight[j + 1], height[j]);
}else {
maxRight[j] = height[j];
}
}
for(int m=1;m<len-1;m++){
maxWater += Math.min(maxLeft[m], maxRight[m]) - height[m];
}
return maxWater;
}
上述代码的时间复杂度为, 空间复杂度为。
单调递减栈解
另一种思路是利用单调递减栈,接水的条件是保证中间小于两边,可以使用一个栈,保证入栈的元素始终小于栈顶元素,如果大于则当前栈顶元素就是一个可以装水的洼,此时栈顶出栈,之后检查栈是否为空(如果只存在右边界不存在左边界也接不了水),不为空则看当前元素的左边界和右边界哪个小,然后乘水洼的长度即可,该方法有点类似水平计算接水空间(题解对应解法一,按行求)。代码如下:
public int trap(int[] height) {
int maxWater = 0;
Stack<Integer> stack = new Stack<>();
for(int i=0;i<height.length;i++){
while(!stack.isEmpty()&&height[i]>height[stack.peek()]){
int cur = stack.pop();
if(stack.isEmpty()){
break;
}
int left = stack.peek();
int min = Math.min(height[i], height[left]) - height[cur];
maxWater += min*(i-left-1);
}
stack.push(i);
}
return maxWater;
}
该方法时间复杂度为,空间复杂度为。