系列文章目录
前言
每天进步一点点!!
好像变懒了,不太愿意记录自己的刷题过程,因为觉得很浪费时间,要自己去总结,去对比,都没时间干题目了。但是转念一想,如果这个题目没有充分搞明白,不会举一反三,那么继续刷题下去的意义是什么,遇到类似的还是不会写,倒不如慢慢的,搞懂,搞透,急什么。毕竟有人它是下笔必得分,有人它是这里一点那里一点,无意义罢了。
一、背景
链接:https://leetcode-cn.com/problems/trapping-rain-water/
来源:力扣(LeetCode)
二、我的思路
本质上说,这不是我的思路。此前偶然看过一个视频是讲这个接雨水的问题的,那个人的思路我恰巧记住了,但是实现起来还是比较费劲,其实就是代码敲得少,不熟悉,各种bug,边界考虑不全。
思路是这样的:
雨水怎么样才能被存储起来呢?就是说,有凹槽。这里把两边的高度,称作为“木板”吧。于是我想到,有凹槽的话,那对应数组值不就是先下降,后上升,找到这个凹槽就可以开始记录水容量了。
这里注意一个问题,下降和上升都不是严格意义的,而是说单调不增和单调不减,简单来说,平的也算嘛。
好了,这样似乎一个循环就可以解决,真美好。可是遇到一个问题,这个凹槽是拘泥于左右两侧的板子决定的储水量,若是后序有更高的板子,那这部分没有考虑到呀。
这可如何是好?
艾,如果有更高的,加上更多的那部分水量,就是用当前可用的板子高度-原先计算储水的高度,再乘以一个宽度,这样就可以计算了呀。可是原来板子的高度用什么记录,每一次计算的时候,新板子的高度都要和原来的板子高度比较嘛?如果板子高度不是马上体现出来呢(比如左侧最大高度为100,只有遇到右侧最大高度接近100的时候,比如99,101这样子,它才会有意义),这个时候就必须重新计算。
我想的办法就是,维护一个数组,记录每个凹槽的局部储水量,并把它的高度存储起来,然后重新遇到要更新之前储水的时候,如果新板子的高度大于当前元素,那么更新,如果不大于,就加上原来凹槽的高度。
好吧,直接来代码,大家可能也不知道我在说啥:
class Solution {
public:
int trap(vector<int>& height) {
/*
*思路:先减后增的数据可以存放雨水
*每次找到左右边界即可,左右边界中选取较小的那个值作为桶高
*/
//存放下标,只记录左中右元素来区分是否属于两边高,中间低的情况
vector<int > my_sta;//只放三个元素
int d[20001]={0};
int p[20001]={0};
int area=0;
bool reduce=false;//标志变量,表示开始下降
bool increase=false;
int start=0;//记录开始存水的位置,就是开始下降的位置
int h=INT_MAX;//记录高度
for (int i=0;i<height.size();i++)
{
if (!reduce && height[i]>0 && i<height.size()-1 &&height[i]>height[i+1])
{
reduce=true;//表示开始下降
h=min(h,height[i]);
start=i;
my_sta.push_back(start);
//快速下降
while ( i<height.size()-1 && height[i]>=height[i+1])
{
i++;
}
}
if (reduce && !increase && i>0 && height[i]>height[i-1] )
{
increase=true;
//快速上升
while ( i<height.size()-1 && height[i+1]>=height[i])
{
i++;
}
h=min(h,height[i]);
}
if (reduce && increase)
{
//reduce=false;//归位
increase=false;
//计算之间的总面积,不含边界
area+=h*(i-start-1);
p[i]=h;//表示边界前是用什么高度计算的
//减去多计算的面积
for (int j=i-1;j>start; j--)
{
//数值比高还高,说明装不了水
if (height[j]>=h)
{
area-=h;
}
else
area-=height[j];
}
if (my_sta.size()>=2)
{
if (height[my_sta[1]]<height[i] && height[my_sta[1]] < height[my_sta[0]])
{
area=d[my_sta[0]];//从头计算
int tmp=0;
int he=0;
he=min(height[i],height[my_sta[0]]);
p[i]=he;//表示边界前是用什么高度计算的
for (int j=i-1;j>my_sta[0]; j--)
{
//如果当前用的边界值不如原来使用的
if (he<=p[j])
{
area=d[j];
break;
}
else
tmp+= (he- height[j])>0 ? (he- height[j]):0;
}
area+=tmp;
}
my_sta.pop_back();//删除中间元素
}
h=height[i];//左边界变为当前元素
start=i;//就是开始下降的边界
//如果左边界不是最高,更换左边界
if(my_sta.size()>0 && height[i] >= height[my_sta[0]])
{
my_sta.clear();
}
d[i]=area;//存放以右边界为止的雨水面积
my_sta.push_back(i);//存放右边界
}
}
return area;
}
};
这么复杂的代码,恐怕只有我自己能看懂了吧,这不是一个好的思路。大家别看了。
三、官方的思路
1.确定当前元素左右最大的板子 + 优化
考虑存水与否,看的是两边的板子哪个短,就是当前储水的高度值,这个很简单。
关于确定两边最大的高度,一个是直接暴力遍历,这样求解该问题的时间复杂度是o(n^2);另一个是事先计算好,存放在数组里,用动态规划的办法,这样时间复杂度就只需要o(n)。
代码这里也贴一下,保证文章的完整度:
class Solution {
public:
int trap(vector<int>& height) {
int n = height.size();
if (n == 0) {
return 0;
}
vector<int> leftMax(n);
leftMax[0] = height[0];
for (int i = 1; i < n; ++i) {
leftMax[i] = max(leftMax[i - 1], height[i]);
}
vector<int> rightMax(n);
rightMax[n - 1] = height[n - 1];
for (int i = n - 2; i >= 0; --i) {
rightMax[i] = max(rightMax[i + 1], height[i]);
}
int ans = 0;
for (int i = 0; i < n; ++i) {
ans += min(leftMax[i], rightMax[i]) - height[i];
}
return ans;
}
};
作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/trapping-rain-water/solution/jie-yu-shui-by-leetcode-solution-tuvc/
来源:力扣(LeetCode)
一个优化的办法就是使用双指针,这样就不用o(n)的空间开销了。
要知道,求解左右最大高度的原因是为了确定当前元素的高度,即更小的那个元素值作为高度。
双指针在遍历的时候,并不是同时走,而是依据当前的左右最大高度值的信息进行判断。
明白一个点,如果说当前左元素值<当前右元素值,说明leftmax<rightmax。
这个点很重要,因为一开始左右最大高度是左右元素的当前值。(从头开始这样执行的话&&leftmax更新是依据当前元素和原来的leftmax比较而来)
当前元素能接的雨水为 leftmax-当前元素值,并且左指针++。
同理,当前左元素值>=当前右元素值,说明leftmax>=rightmax。当前元素能接的雨水为 rightmax-当前元素值,并且右指针–。
代码如下:
class Solution {
public:
int trap(vector<int>& height) {
int ans = 0;
int left = 0, right = height.size() - 1;
int leftMax = 0, rightMax = 0;
while (left < right) {
leftMax = max(leftMax, height[left]);
rightMax = max(rightMax, height[right]);
if (height[left] < height[right]) {
ans += leftMax - height[left];
++left;
} else {
ans += rightMax - height[right];
--right;
}
}
return ans;
}
};
作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/trapping-rain-water/solution/jie-yu-shui-by-leetcode-solution-tuvc/
来源:力扣(LeetCode)
2.单调栈
这个办法的思路就是说,将栈元素放入,如果当前元素小于栈顶元素则入栈(表示可以有机会储水),如果当前元素大于栈顶元素,开始出栈栈顶元素(表示开始计算储水),出栈后继续比较和新的栈顶元素的值,即继续计算储水量。这里可能有人会觉得多算了储水量。比如 3 2 0 2 3,在第二个2的时候计算了一遍储水,在第二个3的时候又来计算一遍,那不是多算了嘛?
其实没有,因为计算储水的高度公式为:当前元素和栈顶元素值中较小值–栈顶元素值。后面的减去栈顶元素值就是为了解决这个问题的。
代码如下:
class Solution {
public:
int trap(vector<int>& height) {
int ans = 0;
stack<int> stk;
int n = height.size();
for (int i = 0; i < n; ++i) {
while (!stk.empty() && height[i] > height[stk.top()]) {
int top = stk.top();
stk.pop();
if (stk.empty()) {
break;
}
int left = stk.top();
int currWidth = i - left - 1;
int currHeight = min(height[left], height[i]) - height[top];
ans += currWidth * currHeight;
}
stk.push(i);
}
return ans;
}
};
作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/trapping-rain-water/solution/jie-yu-shui-by-leetcode-solution-tuvc/
来源:力扣(LeetCode)
总结
好像变懒了,不太愿意记录自己的刷题过程,因为觉得很浪费时间,要自己去总结,去对比,都没时间干题目了。但是转念一想,如果这个题目没有充分搞明白,不会举一反三,那么继续刷题下去的意义是什么,遇到类似的还是不会写,倒不如慢慢的,搞懂,搞透,急什么。毕竟有人它是下笔必得分,有人它是这里一点那里一点,无意义罢了。
其实更好的做法是把官方提供的思路,自己去实现一遍,可我没有,我只是知道了可以那样做,而我并没有复现一次。还是懒啊!!!(也许二刷的时候做?)
喜欢就点个赞叭!