题目描述
LeetCode 42. 接雨水(难度:困难)
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
解题思路:
- 首先,拿到这道题目的时候,我们应该先想,什么情况下才能接到雨水?如果凭空想象会很难,但是结合题目给出的图解就非常清晰了,如果后面的柱子高度比前面的柱子高度小,那么是接不到雨水的,而如果后面的柱子高度是大于前面柱子高度的,那么是有可能接到雨水的(注意只是有可能,后面解题过程中我们就可以看出接不到雨水的情形),想到这一步,就可以把这道题转换一下思路,变成求解在某个数之后,找到第一个比它大的数,这个时候我们就应该想到单调栈
- 回忆一下单调栈的应用场景:求在某个数之后,第一个比本身小的数,可以用单调递增栈(因为要出栈说明当前元素比栈顶元素小);求在某个数之后,第一个比本身大的数,可以用单调递减栈(因为要出栈说明当前元素比栈顶元素大)
- 因此,这里我们用单调递减栈来作为解题工具,维护一个单调递减栈,从左往右顺序遍历柱子高度数组,如果当前柱子高度小于栈顶柱子高度,符合单调栈性质,直接入栈,如果当前柱子高度小于栈顶柱子高度,违反单调栈性质,需要弹出栈中的元素直至符合条件,当前元素才能入栈
- 每当有元素出栈的时候,说明找到比当前栈顶元素柱子高度更高的了,有机会接到雨水,那么又是哪种情形可以接到雨水,哪种情形接不到雨水?答案就是:当有一个元素出栈之后,此时栈不为空,那么可以接到雨水;而如果此时栈变为空,那么接不到雨水(因为构不成一个封闭空间,这里不理解的建议动手画一画图)
- 如果确定能够接收到雨水,那么如何计算接收到的雨水数量呢?当栈顶元素出栈之后,该元素的值就是相当于水平线,需要保存起来,水坑的高度就是当前元素和新栈顶元素的最小值减去刚刚出栈的元素值(相当于要减去一个水平线的参考值);而水坑的宽度就是当前元素和新栈顶元素之间的水平距离(可以用数组的索引求出),最终用宽度乘以高度就是本次接收到的雨水数量
- 注意:当有元素出栈之后,计算了此次接收到的雨水数量,如果栈不为空,并且当前元素的值仍然大于新栈顶元素的值,那么重复同样的操作,再次出栈,直到当前元素可以入栈为止
为了更好的理解这道题目,我再以题目给出的示例详细的列出每一个步骤(虽然有点啰嗦^^)
解题步骤
PS:下面步骤中给出的栈中元素从左到右表示栈底到栈顶
- 当前遍历到的元素为0,此时栈空,直接入栈(栈中元素:0)
- 当前遍历到的元素为1,1>栈顶元素0,不符合单调栈性质,因此栈顶元素0出栈,此时栈变为空,那么接不到雨水,因为要能接到雨水,需要当前元素和新栈顶元素围成一个空间,此时1入栈(栈中元素:1)
- 当前遍历到的元素为0,0<栈顶元素1,符合单调栈性质,直接入栈(栈中元素:1,0)
- 当前遍历到的元素为2,2>栈顶元素0,不符合单调栈性质,因此栈顶元素0出栈,此时栈不为空,可以接到雨水,底部参考值为0,当前元素和新栈顶元素1之间的水平距离为(3-1-1=1),水坑的高度为当前元素和新栈顶元素1二者值小的那个减去底部参考值,即为(1-0=1),总雨水量等于宽度乘以高度(1x1=1);当前栈顶元素1仍小于2,继续出栈,此时栈为空,接不到雨水,那么2入栈(栈中元素:2)
- 当前遍历到的元素为1,1<栈顶元素2,符合单调栈性质,直接入栈(栈中元素:2,1)
- 当前遍历到的元素为0,0<栈顶元素1,符合单调栈性质,直接入栈(栈中元素:2,1,0)
- 当前遍历到的元素为1,1>栈顶元素0,不符合单调栈性质,因此栈顶元素0出栈,此时栈不为空,可以接到雨水,底部参考值为0,当前元素和新栈顶元素1之间的水平距离为(6-4-1=1),水坑的高度为当前元素和新栈顶元素1二者值小的那个减去底部参考值,即为(1-0=1),总雨水量等于宽度乘以高度(1x1=1);当前栈顶元素1等于1,也不符合单调栈性质,继续出栈,此时栈不为空,可以接到雨水,底部参考值为1,当前元素和新栈顶元素2之间的水平距离为(6-3-1=2),水坑的高度为当前元素和新栈顶元素2二者值小的那个减去底部参考值,即为(1-1=0),总雨水量等于宽度乘以高度(1x0=0);此时我们可以得出结论:当前元素值等于栈顶元素值,也是接不到雨水的;此时栈顶元素2>1,1可以入栈(栈中元素:2,1)
- 当前遍历到的元素为3,3>栈顶元素1,不符合单调栈性质,因此栈顶元素1出栈,此时栈不为空,可以接到雨水,底部参考值为1,当前元素和新栈顶元素2之间的水平距离为(7-3-1=3),水坑的高度为当前元素和新栈顶元素2二者值小的那个减去底部参考值,即为(2-1=1),总雨水量等于宽度乘以高度(3x1=3);当前栈顶元素2仍小于3,继续出栈,此时栈为空,接不到雨水,那么3入栈(栈中元素:3)
- 当前遍历到的元素为2,2<栈顶元素3,符合单调栈性质,直接入栈(栈中元素:3,2)
- 当前遍历到的元素为1,1<栈顶元素2,符合单调栈性质,直接入栈(栈中元素:3,2,1)
- 当前遍历到的元素为2,2>栈顶元素1,不符合单调栈性质,因此栈顶元素1出栈,此时栈不为空,可以接到雨水,底部参考值为1,当前元素和新栈顶元素2之间的水平距离为(10-8-1=1),水坑的高度为当前元素和新栈顶元素2二者值小的那个减去底部参考值,即为(2-1=1),总雨水量等于宽度乘以高度(1x1=1);当前栈顶元素2等于2,不符合单调栈性质,2出栈,由前面结论可知是接不到雨水的,当前栈顶元素3大于2,符合单调栈性质,2入栈(栈中元素:3,2)
- 当前遍历到的元素为1,1<栈顶元素2,符合单调栈性质,直接入栈(栈中元素:3,2,1)
- 所有元素都已经遍历完毕,栈不为空并不影响,总的结果就是1+1+3+1=6
// 注意:由于栈中存放的是数组的下标,所以需要用到柱子的高度时,记得转换过来
public class 接雨水 {
public int trap(int[] height) {
// 创建一个单调栈,为了便于后面计算,栈中存放的是数组的下标
LinkedList<Integer> stack = new LinkedList<>();
// 第一个元素先直接入栈
stack.push(0);
// 保存雨水的数量
int sum = 0;
for (int i = 1; i < height.length; i++) {
while (!stack.isEmpty() && height[i] >= height[stack.peek()]) {
// 出栈的元素需要保存起来,作为底部参考值
int temp = height[stack.pop()];
// 判断栈是否为空,为空,接不到雨水,直接break
if (stack.isEmpty()) {
break;
}
// 不为空,计算雨水
int w = i - stack.peek() - 1; // 宽度
int h = Math.min(height[i], height[stack.peek()]) - temp; // 高度
sum += w * h;
}
stack.push(i);
}
return sum;
}
}