1,题目描述
英文
Given n non-negative integers representing the histogram's bar height where the width of each bar is 1, find the area of largest rectangle in the histogram.
Above is a histogram where width of each bar is 1, given height = [2,1,5,6,2,3].
The largest rectangle is shown in the shaded area, which has area = 10 unit.
Example:
Input: [2,1,5,6,2,3]
Output: 10
中文
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
以上是柱状图的示例,其中每个柱子的宽度为 1,给定的高度为 [2,1,5,6,2,3]。
图中阴影部分为所能勾勒出的最大矩形面积,其面积为 10 个单位。
示例:
输入: [2,1,5,6,2,3]
输出: 10
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/largest-rectangle-in-histogram
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
2,解题思路
参考@力扣官方题解【柱状图中最大的矩形】中单调栈的思路。
方法一:单调栈
问题引入
直观的思路:确定一个位置 i 的左右边界,ans = max(ans, (right - left - 1) * height[i] )即可;
那么如何确定边界呢?
- 简单的方法:每次都从当前位置 i 开始向两边扩展,直到高度小于当前柱子位置;(重复的次数太多,且没有利用之前计算的结果)
- 单调栈:栈中的元素全部按照递增/递减的规律排列。栈中总是保存递增元素的索引,当遇到比栈顶元素小的元素时,将栈顶元素依次出栈,直到栈顶元素小于当前元素时,便可确定左边界。这样就可以借助于之前计算的结果,降低时间复杂度;
单调栈的实际含义
接下来详细介绍单调栈的用法,仍以获得左边界为例:
- 栈中元素为柱子的下标。假定原先柱子对应的高度如下:
- 按照单调栈的原理,下标0、1、2对应的高度呈递增状态,故依次入栈。此时栈中元素为[0,1,2];
- 当下标移至 3 时,height[0] < height[3]=2 < height[1] < height[2], 故将2,1依次从栈中弹出。此时栈中元素为[0],0即 下标为 3 柱子对应的左边界;
注意:
- 在实际实现过程中,为了处理边界问题,常常设置哨兵。
- 我的方法是,左边界的哨兵即栈中先存入-1,这样遇到-1就说明已经到达了最左边,不会继续出栈了;右边界的哨兵即栈中先存入height.size(),原理同上;
算法
- 利用单调栈,从左向右扫描数组,获得各个位置的左边界;
- 从右向左扫描数组,获得各个位置的右边界;
- 再次扫描数组,更新最终结果ans = max(ans, heights[i] * (right[i] - left[i] - 1))
方法二:单调栈+常数优化
能不能一次遍历,就可以得到左右边界呢?(来自大佬的灵魂拷问)
当然可以,注意到:当位置 i 被弹出栈时,说明此时遍历到的位置 j 的高度小于height[i],且 i 与 j 之间不存在小于height[i]的柱子(若有的话,i 就已经被提前弹出了)。那么位置 j 就是位置 i 的右边界,左边界就是 i 弹出后的栈顶元素;
即每出栈一个元素,即可同时确定其左右边界;
举例如下:
- 原数组如下,假设当前下标 j = 3,此时栈中元素为[0,1,2];
- 由于height[3] < height[2],元素2出栈,可以确定下标为2的柱子对应的左右边界:此时 j = 3 即为下标2对应的右边界,而出栈后的栈顶元素1,即为下标2对应的左边界;
- 同理height[3] < height[1],元素1也会出栈,具体操作同上;
- height[3] > height[0],元素3放入栈中,此时栈中元素为[0,3];
3,AC代码
方法一:单调栈
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
stack<int> s; // 单调栈 存放下标
vector<int> left(heights.size()), right(heights.size());
// 获得左边界
s.push(-1); // 左边界哨兵为-1
for(int i = 0; i < heights.size(); i++){
int tem = s.top();
while(tem != -1 && heights[tem] >= heights[i]){
s.pop();
tem = s.top();
}
s.push(i); // 将当前柱子下标存入单调栈
left[i] = tem;
}
while(!s.empty()) s.pop();
// 获得右边界
s.push(heights.size()); // 右边界哨兵为heights.size()
for(int i = heights.size() - 1; i >= 0; i--){
int tem = s.top();
while(tem != heights.size() && heights[tem] >= heights[i]){
s.pop();
tem = s.top();
}
s.push(i); // 将当前柱子下标存入单调栈
right[i] = tem;
}
int ans = 0;
for(int i = 0; i < heights.size(); i++){
ans = max(ans, heights[i] * (right[i] - left[i] - 1));
}
return ans;
}
};
方法二:单调栈+常数优化
class Solution {
public:
int largestRectangleArea(vector<int> &heights)
{
stack<int> s;
int ans = 0;
heights.push_back(0); // !!!为了保证最右边的元素也能被弹出且参与比较 故插入0
s.push(-1); // 哨兵 标明左边界
for (int i = 0; i < heights.size(); ++i) {
while (s.top() != -1 && heights[s.top()] >= heights[i]) {
int h = heights[s.top()];
s.pop();
ans = max(ans, (i - s.top() - 1) * h);
}
s.push(i);
}
return ans;
}
};
4,解题过程
第一博
就直接暴力吧,双重循环O(n^2),minHigh指定左右指针划定区间内的最小值,ans记录最大矩形面积:
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
int ans = 0, minHigh = INT_MAX;
for(int i = 0; i < heights.size(); i++){
minHigh = INT_MAX;
for(int j = i; j < heights.size(); j++){
minHigh = min(minHigh, heights[j]);
ans = max(ans, (j - i + 1) * minHigh);
// cout<<ans<<endl;
}
}
return ans;
}
};
第二搏
换一种思路,遍历所有的柱子,过程中,把当前遍历到的柱子(高度为h)作为最低的一根,然后向左右两边扩展,直到高度小于h的柱子,这样就可以划分一个区间,区间乘以h用来更新ans。(类似于寻找最长回文子串中的中心扩展法,时间复杂度仍为O(N^2))
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
int ans = 0;
for(int i = 0; i < heights.size(); i++){
int left = i, right = i;
while(left >= 0 && heights[left] >= heights[i]) left--;
while(right < heights.size() && heights[right] >= heights[i]) right++;
ans = max(ans, heights[i] * (right - left - 1));
}
return ans;
}
};
不出意外,还是超时了
第三搏
欣赏了官方题解,发现第二搏中存在的可以改进的地方:由于每次扩展时,都是从当前位置向两边扩展,而没有利用到之前计算边界的结果,比如:
- 当遍历到下标为2的柱子时,可以得到两个边界值0,3
- 现在下标移至3,若按照第二搏中的方法,左右边界又要从下标3开始左右扩展,但是由于已经记录出了下标2的左右边界,且height[3]<height[2],所以可以直接在下标2的左边界基础之上开始继续向左扩展
维护一个单调栈,栈中总是保存递增元素的索引,当遇到比栈顶元素小的元素时,将栈顶元素依次出栈,这样就可以避免重复的扩展了。
从左向右使用一次单调栈,获得左边界;
从右向左使用一次单调栈,获得右边界;
再次遍历数组,ans = max(ans, heights[i] * (right[i] - left[i] - 1));获得最大矩形面积
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
stack<int> s; // 单调栈 存放下标
vector<int> left(heights.size()), right(heights.size());
// 获得左边界
s.push(-1); // 左边界哨兵为-1
for(int i = 0; i < heights.size(); i++){
int tem = s.top();
while(tem != -1 && heights[tem] >= heights[i]){
s.pop();
tem = s.top();
}
s.push(i); // 将当前柱子下标存入单调栈
left[i] = tem;
}
while(!s.empty()) s.pop();
// 获得右边界
s.push(heights.size()); // 右边界哨兵为heights.size()
for(int i = heights.size() - 1; i >= 0; i--){
int tem = s.top();
while(tem != heights.size() && heights[tem] >= heights[i]){
s.pop();
tem = s.top();
}
s.push(i); // 将当前柱子下标存入单调栈
right[i] = tem;
}
int ans = 0;
for(int i = 0; i < heights.size(); i++){
ans = max(ans, heights[i] * (right[i] - left[i] - 1));
}
return ans;
}
};
时间方面和想象中还是有差距的。。。
第四搏
单调栈+常数优化
只用一次遍历即可求出左右边界!
关键点:当位置 i 被弹出栈时,说明此时遍历到的位置 j 的高度小于height[i],且 i 与 j 之间不存在小于height[i]的柱子(若有的话,i 就已经被提前弹出了);
故左边界为 i 弹出后,栈顶的元素;右边界为当前的位置 j ;高度即弹出的 i 对应的height[i];
class Solution {
public:
int largestRectangleArea(vector<int> &heights)
{
stack<int> s;
int ans = 0;
heights.push_back(0); // !!!为了保证最右边的元素也能被弹出且参与比较 故插入0
s.push(-1); // 哨兵 标明左边界
for (int i = 0; i < heights.size(); ++i) {
while (s.top() != -1 && heights[s.top()] >= heights[i]) {
int h = heights[s.top()];
s.pop();
ans = max(ans, (i - s.top() - 1) * h);
}
s.push(i);
}
return ans;
}
};
好了一点(虽然感觉已经非常简洁了o( ̄┰ ̄*)ゞ)