单调栈,即栈中元素是单调递增的或是单调递减的,是一个比较好用的数据结构.
柱状图中最大的矩形
84. 柱状图中最大的矩形 - 力扣(LeetCode)
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
注意到,当从左向右看,并尝试计算面积时,总是在前一个比后一个大的时候才会尝试计算, 如果柱子高度是单增的,那么显然会有更大的面积,只有变矮时,才不会右更大的面积.
那么,我们可以创建一个单增的栈,里面存储各个柱的序号,如果后续时单增的,就一直入栈,如果当前元素比栈顶元素小,就出栈,并计算面积.这样只需要一次遍历就可以计算得出.
(画红虚线的为最终栈中元素)
注意边界问题,在遍历一次柱状图后,栈中可能还会有剩余元素.显然它们是单增的,那么从右往左看,直到栈中的前一个元素,都是可计算面积的.即
length = res.top();res.pop();//pop后的栈顶元素即为约束width = res.empty()?n:n-res.top()-1;//已经出栈,则宽度多算了1,为空的话,则说明这个元素是最小的.
完整代码:
class Solution {
public:int largestRectangleArea(vector<int>& heights) {unsigned long n = heights.size();if (n == 1){return heights[0];}stack<int> res;int ans=0;//使用一个单增的栈,一遇到单减,立马出栈,计算面积for(int i=0;i<n;i++){while(!res.empty()&&heights[res.top()]>heights[i]){int length=heights[res.top()];res.pop();//使用后立即出栈,良好习惯int width = res.empty()?i:i-res.top()-1;//注意已经出栈,宽度比实际面积大1,手动减去ans = max(ans,length*width);}res.push(i);}//完成后可能栈非空,再来一次while(!res.empty()){int length=heights[res.top()];res.pop();int width =res.empty()?n : n -res.top()-1;ans = max(ans,length*width);}return ans;}
};
接雨水
42. 接雨水 - 力扣(LeetCode)
给定 n个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
注意到当柱子单减时,突然变大了,则可以接雨水,可以使用单减栈(注意接雨水需要墙,而边界不算墙),判断是否能接水,至少需要三根柱子: c u r , l e f t , r i g h t cur,left,right cur,left,right.
c u r cur cur表示判断可能可以接水的地方,即他的下一个柱子即 r i g h t right right,是比它高的,那么认为, l e f t ∼ r i g h t left\sim right left∼right这片区域(不含边界,是开的)的高度均为cur,为什么可以这样认为后续会进一步讲解.那么这三根柱子带来的雨水收益为: ( m i n ( l e f t , r i g h t ) − h e i g h t [ c u r ] ) ∗ ( r i g h t − l e f t − 1 ) ) (min(left,right)-height[cur])*(right-left-1)) (min(left,right)−height[cur])∗(right−left−1))
现在解释为什么可以认为 l e f t ∼ r i g h t left\sim right left∼right这片区域可认为高度均为cur:
- 显然这片区域没有比 c u r cur cur更高的柱子
- 如果高度均为 h e i g h t [ c u r ] height[cur] height[cur],那么显然成立
- 如果有比 c u r cur cur低的柱子 p p p,那么在区间 [ c u r , r i g h t ] [cur,right] [cur,right],可认为高度均为 p p p,已经被计算过雨水面积为 ( m i n ( c u r , r i g h t ) − p ) ∗ ( r i g h t − c u r − 1 ) (min(cur,right)-p)*(right-cur-1) (min(cur,right)−p)∗(right−cur−1),也就是说那些实际比cur低的但在此次计算中被认为是柱子的空白部分,是被计算过了的.
这是一个递归的过程,如果不太能理解,可以看例子中的序号3到6这一区间的计算过程,你会发现在计算序号5的柱子接水时,加且仅加了1,在后续的计算中,这一方格被自动地认为是柱子,不再认为是空白.
class Solution {
public:int trap(vector<int>& height) {//注意到,只要单减时遇到变大,就可以接水,考虑单调栈stack<int> st;int res=0;int len=height.size();for(int i=0;i<len;i++){while(!st.empty()&&height[st.top()]<height[i]){int cur=st.top();st.pop();if(st.empty())break;//由于接水需要左右两边都有墙,如果弹出后为空,说明左边没墙,结束,否则溢出int l=st.top();int r=i;int h=min(height[l],height[r])-height[cur];res+=(r-l-1)*h;}st.push(i);}return res;}
};
最大矩形
85. 最大矩形 - 力扣(LeetCode)
给定一个仅包含 0 和 1 、大小为 $rows \times cols $ 的二维二进制矩阵,找出只包含 1 的最大矩形,并返回其面积。
注意到这是一个二维的单减栈(最小栈)问题,即每一行都用单调栈处理一次即可.
相当于是每一行都是一个柱状图,而柱状图高度取决于有多少个连续的1,使用height[cols]来维护.
class Solution {
public:int maximalRectangle(vector<vector<char>>& matrix) {if(matrix.empty())return 0;int rows=matrix.size();int cols=matrix[0].size();int maxarea=0;vector<int> height(cols,0);//对每一行都进行计数,并通过最小栈计算出每一行最大的矩形for(int i=0;i<rows;i++){for(int j=0;j<cols;j++){if(matrix[i][j]=='1')height[j]++;else height[j]=0;}//注意一列中必须有连续的1才能++,否则就更新为0stack<int> st;for(int j=0;j<cols;j++){while(!st.empty()&&height[st.top()]>height[j]){int length = height[st.top()];st.pop();int weight = st.empty()?j:j-st.top()-1;maxarea=max(maxarea,length*weight);}st.push(j); }while(!st.empty()){int length = height[st.top()];st.pop();int weight = st.empty()?cols:cols-st.top()-1;maxarea=max(maxarea,length*weight);}}return maxarea;}
};
单调栈模板
//单减栈(最小栈)for(int i =0;i<len;i++){while(!st.empty()&&heights[st.top()]>heights[i]){//单增栈(最大栈)只需要改成小于就好int height = heights[st.top()];st.pop();int width = st.empty()?i:i-st.top()-1;area = height*width}st.push(i);}//如果需要处理栈中剩余元素,则还需要while(!st.empty()){int height = heights[st.top()];st.pop;int width = st.empty()?len:len-st.top()-1;area = height*width;}