这个题算是典型题了,很多笔试题或者面试的时候都会考这个的变体。
先写个暴力看一看,思路在注释里:
class Solution {
public:
int trap(vector<int>& height) {
int res = 0;
for(int i = 1; i < height.size(); ++i){//对于每一列
int left_m = height[i], right_m = left_m;
//左边的最大值
for(int j = 0; j < i; ++j) left_m = max(left_m, height[j]);
//右边的最大值
for(int j = i+1; j < height.size(); ++j) right_m = max(right_m, height[j]);
int m = min(left_m, right_m);//取两者之间较小的
res += m > height[i] ? m-height[i] : 0;
}
return res;
}
};
能够通过314个测试用例,最后一个超时,至少说明思路是对的。
下面就需要考虑怎么优化,因为暴力法重复计算很多,每一次都需要计算出两边的最大最小值,那先来简单的:如果计算出全局最大值,那么全局最大值的左边元素的右边的最大值是不是就不用计算了呢?同理右边元素的左边最大值不用计算了,因为必然是全局最大值。虽然只是小优化,但是可以让我们ac,笔试的时候或许能帮上大忙:
class Solution {
public:
int trap(vector<int>& height) {
int res = 0;
int idx = max_element(height.begin(), height.end()) - height.begin();//最大元素的下标
for(int i = 1; i < height.size(); ++i){
if(i == idx) continue;
int left_m = height[i], right_m = left_m;//左右两边的最大值
if(i < idx){
//左边的最大值
for(int j = 0; j < i; ++j) left_m = max(left_m, height[j]);
right_m = height[idx];//右边最大值肯定是全局最大值
} else{
left_m = height[idx];//左边最大值肯定是全局最大值
//右边的最大值
for(int j = i+1; j < height.size(); ++j) right_m = max(right_m, height[j]);
}
int m = min(left_m, right_m);//取两者之间较小的
res += m > height[i] ? m-height[i] : 0;
}
return res;
}
};
现在我们至少是通过了(260ms)。
再接着优化吧,试想如果我们计算出数组三等分点的三个最大值,是不是又能减少计算量了,至少对于三等分的中间部分,左右的最大值都可以直接取了。那如果是四等分,五等分呢?想想都有点激动,不过计算最大值本身就是比较耗时的。再想想,我们在从左往右遍历的时候,其实我们已经访问过每个元素了,但是访问过后就丢弃了,当我们考虑后面的元素的时候,又需要回来再遍历寻找最大值。那我们可不可以遍历的时候,就记录下每个位置左边的最大值是多少呢?当然是可以的,其实这就是单调栈的思路。
先写下来看一看:
这儿需要注意右边的单调栈需要从右往左遍历进行维护,所以需要先逆序遍历一次把右边的单调栈计算出来,而左边的单调栈可以一边计算雨水值得时候一边维护(当然也可以事先计算出来,区别并不大):
class Solution {
public:
int trap(vector<int>& height) {
int res = 0;
vector<int> m_l(height);//维护左边最大值的单调栈,m[i]是下标i的元素的左边元素的最大值
vector<int> m_r(height);//维护右边最大值的单调栈,m[i]是下标i的元素的右边元素的最大值
for(int i = height.size()-2; i >= 0; --i){//维护右边单调栈(逆序遍历)
if(height[i] < m_r[i+1]) m_r[i] = m_r[i+1];
else m_r[i] = height[i];
}
for(int i = 1; i < height.size(); ++i){
//维护左边单调栈
if(height[i] < m_l[i-1]) m_l[i] = m_l[i-1];
else m_l[i] = height[i];
int left_m = m_l[i];//左边的最大值
int right_m = m_r[i];//右边的最大值
int m = min(left_m, right_m);//取两者之间较小的
res += m > height[i] ? m-height[i] : 0;
}
return res;
}
};
优化以下代码逻辑:
class Solution {
public:
int trap(vector<int>& height) {
int res = 0;
//维护左(右)边最大值的单调栈,m[i]是下标i的元素的左(右)边元素的最大值
vector<int> m_l(height), m_r(height);
for(int i = height.size()-2; i >= 0; --i)//维护右边单调栈(逆序遍历)
m_r[i] = height[i] < m_r[i+1] ? m_r[i+1] : height[i];
for(int i = 1; i < height.size(); ++i){
m_l[i] = height[i] < m_l[i-1] ? m_l[i-1] : height[i];//维护左边单调栈
int m = min(m_l[i], m_r[i]);//取两者之间较小的
res += m > height[i] ? m-height[i] : 0;
}
return res;
}
};
ok,现在得效率就可以达到4ms了。
题解里还看到一个递减栈的思路,一次遍历就可以,有点像括号匹配的思路:
【接雨水】单调递减栈,简洁代码,动图模拟 - 接雨水 - 力扣(LeetCode)
class Solution {
public:
int trap(vector<int>& height) {
int res = 0, cur = 0;
stack<int> s;//相当于是递减栈
while(cur < height.size()){
while(!s.empty() && height[cur] > height[s.top()]){
int top = s.top(); s.pop();
if(s.empty()) break;//只有一个,无法积水
int dis = cur - s.top()-1;//宽
int bound_height = min(height[cur], height[s.top()]) - height[top];//高
res += dis*bound_height;
}
s.push(cur++);
}
return res;
}
};