接雨水
问题描述:给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
示例:
输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。
我们使用单调栈来解决该问题。
什么是单调栈?
单调栈是一种特殊的栈结构,它维护了栈内的元素是单调递增或单调递减的顺序。
1.单调递增栈:栈内的元素始终保持单调递增的顺序,即对于任意的 i < j,都有 stack[i] <= stack[j]。
2.单调递减栈:与单调递增栈相反,栈内的元素保持单调递减的顺序,即对于任意的 i < j,都有 stack[i] >= stack[j]。
单调栈的操作:
入栈操作:将元素压入栈中,如果新元素不违反栈的单调性,则直接入栈;如果违反单调性,则先弹出栈顶元素,直到栈顶元素满足单调性,然后再将新元素压入。
出栈操作:从栈中弹出元素,通常用于维护栈的单调性。
查询操作:在单调栈中,可以快速查询给定元素的下一个更大/更小元素,因为单调栈保证了元素的单调性,所以查询操作可以非常高效。
总结来说,我们对单调栈的各种操作,都是为了让栈保持单调性,便于我们快速查询下一个更大/更小元素。
单调栈的实现:
单调栈通常通过一个辅助栈来实现,该辅助栈用于存储索引而不是实际的数值。在处理数组时,辅助栈中的每个元素都对应数组中的一个索引,并且维护了单调性。
例如,在接雨水问题中,单调栈用于维护已遍历过的柱子的索引,使得栈顶元素总是当前遍历到的柱子左侧最高的柱子。当遇到一个比栈顶元素更高的柱子时,就可以计算出栈顶元素及其左侧柱子之间的雨水量。
接下来是接雨水中单调栈的具体用法:
我们使用一个单调栈st 来存储遍历过程中遇到的柱子的索引。
从左到右遍历数组 height。在遍历过程中,如果栈不为空且栈顶元素对应的柱子高度小于当前遍历到的柱子高度,说明当前柱子可以接住雨水。
当栈顶元素对应的柱子高度小于当前柱子高度时,我们从栈中弹出一个元素,这意味着我们找到了一个可以接住雨水的区域。
弹出元素后,如果栈为空,说明我们到达了数组的开始,此时跳出循环。
如果栈不为空,计算当前可以接住雨水的高度 h,它是左右两边柱子高度的最小值与弹出柱子高度的差。
计算当前区域的雨水量,即 h 乘以当前柱子与栈顶柱子之间的水平距离(right - left - 1)。
最后将当前遍历到的柱子索引压入栈中。
通俗来说:
从左到右遍历数组,对于每个柱子,我们尝试将其索引压入栈中。
如果栈不为空,并且当前柱子的高度大于栈顶柱子的高度,这意味着当前柱子可以作为栈顶柱子的“盖子”,帮助它接住雨水。
我们从栈中弹出所有这样的柱子(即当前柱子高度大于栈顶柱子高度的柱子),因为它们现在找到了更高的“盖子”。所以代码中这里使用的是while循环。
每次弹出时,我们计算被弹出柱子能够接住的雨水量,并累加到结果中。
下面是例子的过程,我们模拟一遍。
如果看不懂,试着多看几遍,hard题当然有难度
i = 0:height[0] = 0,入栈,st = [0]。
i = 1:height[1] = 1,大于栈顶(0),弹出栈顶元素(0),此时栈空,根据算法逻辑,栈空则不计算雨水量,直接跳过当前循环,1 入栈,st = [1]。
i = 2:height[2] = 0,小于栈顶(1),直接入栈,st = [1, 2]。
i = 3:height[3] = 2,大于栈顶(1 和 2),开始计算雨水量:
(1)弹出 2,此时栈顶为 1,left = 1,right = 3,计算h=min(height[1], height[3]) - height[2] = min(1, 2) - 0 = 1,该区域雨水量为 (3 - 1 - 1) * 1 = 1。
(2)弹出 1,栈空,跳出当前循环。
(3)3 入栈,st = [3],res = 1。
i = 4:height[4] = 1,小于栈顶(3),直接入栈,st = [3, 4]。
i = 5:height[5] = 0,小于栈顶(4),直接入栈,st = [3, 4, 5]。
i = 6:height[6] = 1,大于栈顶(5),开始计算雨水量:
(1)弹出 5,此时栈顶为 4,left = 4,right = 6,计算雨水量h=min(height[4], height[6]) - height[5] = min(1, 1) - 0 = 1,该区域雨水量为 (6 - 4 - 1) * 1 = 1。
(2)再比较height[6]和栈顶4,并不大于,所以不计算雨水量
(3)6入栈,st = [3,4,6],res = 2。
i = 7:height7] = 3,大于栈顶(6),开始计算雨水量:
(1)弹出 6,此时栈顶为 4,left = 4,right = 7,计算雨水量h=min(height[4], height[7]) - height[6] = min(1, 3) - 1 = 0,该区域雨水量为 (7 - 4 - 1) * 0 = 0。此时栈st=[3, 4]
(2)再比较height[7]和栈顶4,大于,计算雨水量:
(2.1)弹出 4,此时栈顶为 3,left = 3,right = 7,计算雨水量h=min(height[3], height[7]) - height[4] = min(2, 3) - 1 = 1,该区域雨水量为 (7 - 3 - 1) * 1= 3。
(2.2)再比较height[7]和栈顶3,大于,弹出栈顶3,但此时栈为空,不计算雨水量
(3)7入栈,st = [7],res = 5。
i = 8:height[8] = 2,小于栈顶(7),直接入栈,st = [7, 8]。
i = 9:height[9] = 1,小于栈顶(8),直接入栈,st = [7, 8, 9]。
i = 10:height[10] = 2,大于栈顶(9),开始计算雨水量:
(1)弹出 9,此时栈顶为 8,left = 8,right = 10,计算雨水量h=min(height[8], height[10]) - height[9] = min(2, 2) - 1 = 1,该区域雨水量为 (10 - 8 - 1) * 1 = 1。此时栈st= [7, 8]
(2)再比较height[10]和栈顶8,不大于,10入栈,st= [7, 8,10],res=6
i = 11:height[11] = 1,小于栈顶(10),11入栈,st= [7, 8,10,11],res=6
遍历结束,得到答案6!
如果你看完全过程,相信你对单调栈的使用有了基本的理解了,下面是具体代码:
class Solution {
public:
int trap(vector<int>& height) {
int size = height.size(); // 获取数组的长度
if (size == 0) return 0; // 如果数组为空,则返回0
int res = 0; // 结果初始化为0
stack<int> st; // 使用栈来辅助计算
for (int i = 0; i < size; i++) // 遍历数组
{
while (!st.empty() && height[st.top()] < height[i]) // 如果栈非空且当前元素大于栈顶元素
{
int top = st.top(); // 取出栈顶元素
st.pop(); // 弹出栈顶元素
if (st.empty()) break; // 如果弹出之后栈变为空,则跳出循环
int left = st.top(); // 获取新的栈顶元素
int right = i; // 当前位置为右边界
int h = min(height[left], height[right]) - height[top]; // 计算当前位置能够接到的雨水高度
res += (right - left - 1) * h; // 计算当前位置能够接到的雨水体积并累加到结果中
}
st.push(i); // 将当前位置入栈
}
return res; // 返回最终的结果
}
};
可能你对代码中的部分位置有些疑问,比如:
1.为什么遇到当前元素大于栈顶元素进行雨水量计算的时候,要先把原栈顶元素弹出再用新的栈顶元素计算雨水量?
你想想,我们算雨水量是不是要用两个盖子把雨水夹起来?所以算的时候我们使用两侧的高度。
2.为什么弹出栈顶元素栈为空,跳过此次计算?
栈为空,代表两个盖子中的左盖子没有了!你想想,只有右侧盖子,怎么夹住雨水?
如果你还有任何疑问,请在下方评论区告诉我。
做完这道题,是不是感觉你已经对单调栈完全掌握了?现在你可以试试其他题,也是使用单调栈这个知识点
leetocde461 | 下一个更大元素 I |
---|---|
leetocde503 | 下一个更大元素 II |
leetocde84 | 柱状图中最大的矩形 |