代码随想录算法训练营第60期第五十二天打卡
大家好,昨天我们重点讲解了单调栈的问题,我们今天的题目还是继续围绕单调栈展开,我们上节课其实对单调栈已经有了大致的了解,今天的第一道题目大家务必要注意很重要,接雨水问题我们会涉及到单调栈与双指针,这道题目可以说是面试中的高频题目,好了那我们就走进今天的题目。
第一题对应力扣编号为42的题目接雨水
这道题目我们以前没有接触过类似的题目,那我们就直接看题目要求:
看完题目我明白了这道题目的含义,我们是要求我们能接多少雨水,大家看上面这幅示意图,其实两根柱子之间首先不能相邻那么其实可以接的雨水数量是那根短的柱子的长度,其实就是短板效应,但是重点还得考虑中间的这种情况,这种情况不太容易描述,如果有两个相邻的柱子不相邻的话其实可以分开看,其实中间部分有一个高度为1的柱子,它们的周围就有高度为2和3的柱子而且中间还隔了一个高度为0的柱子,大家就暂时这样理解,想接到雨水两边就都要有柱子来挡着防止流走,那我们就看看这道题目的解题思路:
首先我们还是先来看暴力的一种解法,其实可以分为两种思路一种是按行求另一种是按列求,大家看示意图:
那这里我们使用按列求,因为似乎按列求比较容易理解,我们是可以确定宽度都是1的,然后我们也会慢慢发现规律,我们每一列可以接的雨水单位数量就是其左侧最高的柱子高度与右侧最高的柱子高度的高度差再减去当前列的柱子高度就可以了,比如说我们求列4的雨水高度,列4 左侧最高的柱子是列3,高度为2(以下用lHeight表示)。列4 右侧最高的柱子是列7,高度为3(以下用rHeight表示)。列4 柱子的高度为1(以下用height表示),那么列4的雨水高度为 列3和列7的高度最小值减列4高度,即: min(lHeight, rHeight) - height。其实这样就可以了,列4的雨水高度求出来了,宽度为1,相乘就是列4的雨水体积了。那么接下来我们就可以遍历所有的列,注意我们的第一列与最后一列是一定不可以接到雨水的,根据规律我们就可以给出解题代码:
class Solution {
public:int trap(vector<int>& height) {//保存最后的雨水的单位数int sum = 0;for (int i = 0; i < height.size(); ++i){//注意我们第一列与最后一列是不接雨水的if (i == 0 || i == height.size() - 1) continue;int rheight = height[i];//存储右边最高的柱子int lheight = height[i];//存储左边最高的柱子//找到右边最高的柱子for (int r = i + 1; r < height.size(); ++r){if (height[r] > rheight) rheight = height[r];}//找到左边最高的柱子for (int l = i - 1; l >= 0; --l){if (height[l] > lheight) lheight = height[l];}int h = min(lheight, rheight) - height[i];if (h > 0) sum += h;}return sum;}
};
这样虽然是超时的,但是我希望大家可以理解这种思路,我们接下来就看不超时的例子,其实我们是可以考虑使用双指针进行优化的,我们为了找到左边的柱子最高值与右边柱子的最高值我们需要遍历两次,为了得到两边的最高高度,使用了双指针来遍历,每到一个柱子都向两边遍历一遍,这其实是有重复计算的。我们把每一个位置的左边最高高度记录在一个数组上(maxLeft),右边最高高度记录在一个数组上(maxRight),这样就避免了重复计算。我们来看一下我们的双指针思路:
class Solution {
public:int trap(vector<int>& height) {if (height.size() <= 2) return 0;//定义两个数组来分别存储每一个柱子的左侧的柱子高度最大值与右侧的柱子高度的最大值vector<int> maxLeft(height.size(), 0);vector<int> maxRight(height.size(), 0);int size = maxRight.size();//记录左边柱子的最大高度maxLeft[0] = height[0];for (int i = 1; i < height.size(); ++i){maxLeft[i] = max(height[i], maxLeft[i - 1]);}//记录右边柱子的最大高度(注意我们这里要倒序遍历)maxRight[size - 1] = height[size - 1];for (int i = size - 2; i >= 0; --i){maxRight[i] = max(height[i], maxRight[i + 1]);}//求和int sum = 0;for (int i = 0; i < size; ++i){int count = min(maxLeft[i], maxRight[i]) - height[i];if (count > 0) sum += count;}return sum;}
};
这种解法是可以通过本题的,大家要理解,当然我们今天的主题是单调栈,我们还是要学单调栈如何解决这个问题:首先单调栈是按照行方向来计算雨水,我们上面已经给出过按行方向的示意图,还得考虑使用单调栈内元素的顺序,其实也很明显就是使用从小到大的顺序,因为一旦发现添加的柱子高度大于栈头元素了,此时就出现凹槽了,栈头元素就是凹槽底部的柱子,栈头第二个元素就是凹槽左边的柱子,而添加的元素就是凹槽右边的柱子。如果遇到相同的元素,更新栈内下标,就是将栈里元素(旧下标)弹出,将新元素(新下标)加入栈中。因为我们要求宽度的时候 如果遇到相同高度的柱子,需要使用最右边的柱子来计算宽度。还有一个问题我们需要理解,我们其实不需要pair来存储每根柱子的高度与下标,我们其实直接存放下标就可以,我们通过下标就可以访问到对应的高度,这样我们就可以尝试给出本思路的解题代码:
class Solution {
public:int trap(vector<int>& height) {if (height.size() <= 2) return 0;stack<int> st; //存着下标,计算的时候用下标对应的柱子高度st.push(0);//存储最终的和int sum = 0;for (int i = 1; i < height.size(); ++i){if (height[i] < height[st.top()]){st.push(i);} else if (height[i] == height[st.top()]){st.pop();st.push(i);}else{while(!st.empty() && height[i] > height[st.top()]){int mid = st.top();st.pop();if (!st.empty()){int h = min(height[i], height[st.top()]) - height[mid];int w = i - st.top() - 1; //注意减1求中间宽度sum += h * w;}}st.push(i);}}return sum;}
};
这种思路比较难,大家注意理解。
第二题对应力扣题号为84的题目柱状图中最大的矩形
这道题目其实如果以前没有接触过单调栈的话就很难了,但是如果我们接触过单调栈的话或许就不难了,我们还是先看一下题目的具体要求:
这道题目其实与上一道题目有些类似,其实那里要收集雨水的话是有条件的,但在这里其实凑出最大的矩形也是有条件的,其实还有点难,因为我的柱子可以高但如果不够宽的话会不会就比虽说柱子不够高但是够宽的小了,这点大家是不是需要留意一下?我们就看看这道题目我们如何使用单调栈来解决?其实还是可以使用暴力解法的,我可以直接给大家解题代码我就不解释了,因为暴力解法是超时的,大家见下面的解题代码:
class Solution {
public:int largestRectangleArea(vector<int>& heights) {//表示的是矩形的面积int sum = 0;for (int i = 0; i < heights.size(); ++i){int left = i;int right = i;for (; left >= 0; --left){if (heights[left] < heights[i]) break;}for (; right < heights.size(); ++right){if (heights[right] < heights[i]) break;}int w = right - left - 1;int h = heights[i];sum = max(sum, w * h);}return sum;}
};
接下来我们重点看单调栈的做法:因为本题是要找每个柱子左右两边第一个小于该柱子的柱子,所以从栈头(元素从栈头弹出)到栈底的顺序应该是从大到小的顺序!其实其他的地方与上面的一题接雨水都一样了,其实接雨水那道题目我们是不是求右边第一个比它大的元素,而这道题目我们是不是应该是求右边第一个比它小的元素,注意我们以每一个柱子的高度为基准的话什么时候可以凑出矩形,有些情况我们是不可以往左或者往右延展的,比如说题目示例里面我们5是不是无法向左边延展,因为向左根本就凑不出矩形。
本题目还有一件很重要的事情,我如果要去求比如说右边的比自身小的第一个元素,那么我当前给出的数组如果就是一个单调递增的数组,那我还有收获结果的过程吗?其实就没有了,因为我什么时候可以收获结果,很明显是当前元素的值小于我栈顶元素的时候才可以,因为我们就会考虑在结尾处加一个0,这样我们才可以收获结果,同理如果我的数组是一个单调递减的数组,那其实我就需要在开头加一个0,如果数组本身是降序的,例如 [8,6,4,2],在 8 入栈后,6 开始与8 进行比较,此时我们得到 mid(8),right(6),但是得不到 left。为了避免出现空栈就存在这样了这样的操作,接下来我们就尝试给出我们的解题代码:
class Solution {
public:int largestRectangleArea(vector<int>& heights) {int result = 0;stack<int> st;heights.insert(heights.begin(), 0); // 数组头部加入元素0heights.push_back(0); // 数组尾部加入元素0st.push(0);// 第一个元素已经入栈,从下标1开始for (int i = 1; i < heights.size(); i++) {if (heights[i] > heights[st.top()]) { // 情况一st.push(i);} else if (heights[i] == heights[st.top()]) { // 情况二st.pop(); // 这个可以加,可以不加,效果一样,思路不同st.push(i);} else { // 情况三while (!st.empty() && heights[i] < heights[st.top()]) { // 注意是whileint mid = st.top();st.pop();if (!st.empty()) {int left = st.top();int right = i;int w = right - left - 1;int h = heights[mid];result = max(result, w * h);}}st.push(i);}}return result;}
};
今日总结
今天的单调栈其实不简单,有时候我们会分不清什么时候使用递增的单调栈,什么时候使用递减的单调栈,这个其实取决于我们的题目要求,如果我们是需要求比如右边第一个大于本身的数我们就需要使用单调递增的单调栈,如果我们要求右边第一个小于本身的数我们就需要使用单调递减的单调栈,还有一个问题我们什么时候是收获结果的时候,希望大家可以理解,我们今天的分享就到这里了,明天我们就会开始图论,大家明天见!