🙉饭不食,水不饮,题必须刷🙉
C语言免费动漫教程,和我一起打卡! 🌞《光天化日学C语言》🌞
LeetCode 太难?先看简单题! 🧡《C语言入门100例》🧡
数据结构难?不存在的! 🌳《数据结构入门》🌳
LeetCode 太简单?算法学起来! 🌌《夜深人静写算法》🌌
究极算法奥义!深度学习! 🟣《深度学习100例》🟣
一、题目
1、题目描述
在一个矩形的直方图上,有 n ( n ≤ 1 0 5 ) n(n \le 10^5 ) n(n≤105) 个方柱,每个方柱的高度为 h [ i ] ( h [ i ] ≤ 1 0 4 ) h[i] (h[i] \le 10^4) h[i](h[i]≤104),宽度为 1。求一个内接矩形,使得它的面积最大。如图1所示的直方图的每一个矩形高度为 2, 1, 4, 5, 1, 3, 3
图1 最大的内接矩形的面积为 4 × \times × 2 = 8,即图2中红色部分的面积。![]()
图2
样例输入: [ 2 , 1 , 4 , 5 , 1 , 3 , 3 ] [ 2, 1, 4, 5, 1, 3, 3 ] [2,1,4,5,1,3,3]
样例输出: 8 8 8
2、基础框架
- c++ 版本给出的基础框架代码如下:
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
}
};
3、原题链接
( 1 ) (1) (1) LeetCode 84. 柱状图中最大的矩形
( 2 ) (2) (2) 剑指 Offer II 039. 直方图最大矩形面积
二、解题报告
1、思路分析
1. 朴素做法
- 首先,提供一个最暴力的办法,然后再来逐渐优化。
- 枚举一个左方柱 l l l 和一个右方柱 r r r ,然后用 RMQ 区间最值求出区间的高度最小值 h m i n ( l , r ) hmin(l, r) hmin(l,r),那么答案就是: m a x ( ( r − l + 1 ) ∗ h m i n ( l , r ) ) max( (r-l+1)*hmin(l,r) ) max((r−l+1)∗hmin(l,r)) ( 1 ≤ l ≤ r ≤ n ) (1 \le l \le r \le n) (1≤l≤r≤n)
- 这样做存在的问题就是时间复杂度是 O ( n 2 ) O(n^2) O(n2) 的,而这个问题中 n n n 很大,我们需要想 O ( n ) O(n) O(n) 或者 O ( n l o g n ) O(nlogn) O(nlogn) 的方法。
2. 枚举左方柱
- 延续之前的思路,我们还是枚举最左的那个方柱
l
l
l,那么它能够作为 “最左的那个方柱” 是有条件限制的,如图3所示:
图3 - 图中的方柱高度分别为 1,2,2,4,3 当枚举的 “最左的那个方柱” 为图中蓝色方柱(第四个方柱),那么以它为最左方柱的内接矩形的高度一定要大于它前一个方柱的高度,不然前一个方柱作为 “最左的那个方柱” ,得到的面积肯定会更大。
图4 - 如上图所示,以第四个方柱作为左端点,向右扩展时, ( a ) 、 ( b ) (a)、(b) (a)、(b) 合法, ( c ) (c) (c) 非法(因为它的左方柱可以继续往左延伸)。
3. 固定右方柱
- 然后,我们来看这么一种情况,从左往右扫描,扫描到第
i
i
i 个方柱,前面的方柱都是单调不减的,并且
h
(
i
−
1
)
>
h
(
i
)
h(i-1) > h(i)
h(i−1)>h(i),如图5所示,蓝色方柱就是第
i
i
i 个方柱。
图5 - 这时候我们找到了几个满足 h ( l ) > h ( i ) h(l) > h(i) h(l)>h(i) 的最左方柱 l l l,并且计算了几块候选面积,如图6所示:
- 上图中的三种情况都是合法的,并且我们并不知道它们哪个面积大(红色代表每种方案的内接矩形的面积),试想一下图 ( a ) (a) (a) 中第六个方柱的高度无限高,那么它的面积明显就会比 图 ( b ) (b) (b)、图 ( c ) (c) (c)中的大,反之则不一定;
- 对于所有的
h
(
l
)
>
h
(
i
)
h(l) > h(i)
h(l)>h(i) 的这些情况我们都需要计算一次,而且只需要计算一次,因为他们能够到达的最右方柱就是
r
=
i
−
1
r = i-1
r=i−1,和第
i
−
1
i-1
i−1 个方柱以后的方柱已经完全没有关系了,并且我们可以把计算后的图形和图7进行等价:
图7 - 于是,我们发现扫描完第 i i i 个方柱以后,前面的形状又变成了一个单调不减的,所以这个过程就变成了不断得维护一个单调不减的序列,严格一点,应该说是单调递增,因为高度相同的情况,下标越小越优。
- 这一步我们称为削峰。
4. 维护单调递增序列
- 通过一个 单调栈 st 来维护一个单调递增序列,栈里存的是 每个位置的下标,高度在栈内从栈底到栈顶单调递增。
- 从左往右扫描每个方柱,对于一个栈 s,令 栈顶元素为 s t . t o p ( ) st.top() st.top() 考虑两种情况:
- 1)空栈 或者 栈顶元素的高度 小于 当前元素的高度,即
h
[
s
t
.
t
o
p
(
)
]
<
h
[
i
]
h[st.top()] < h[i]
h[st.top()]<h[i] 时,入栈;
- 2)否则,循环判断栈顶元素,如果 栈顶元素的高度 大于等于 当前元素的高度,即
h
[
s
t
.
t
o
p
(
)
]
≥
h
[
i
]
h[st.top()] \ge h[i]
h[st.top()]≥h[i],出栈,并且更新最大面积 :
a n s = m a x ( a n s , ( i − s t . t o p ( ) ) ∗ h [ s t . t o p ( ) ] ) ans = max(ans, (i - st.top()) * h[st.top()] ) ans=max(ans,(i−st.top())∗h[st.top()]) - 然后将最后出栈的那个元素继续入栈,并且将它的高度设置为 h [ i ] h[i] h[i] ,也就是上文提到的削峰。
- 3)为了让所有元素都能出栈,我们需要在原高度最后面插入一个最小的数,即 -1。
- 有关 栈 的实现,可以参见以下文章:《画解数据结构》栈。
- 有关 单调栈 更多的内容,可以看以下这篇文章:夜深人静写算法(十一)- 单调栈。
2、时间复杂度
- 由于每个元素最多入栈一次,出栈一次。
- 所以时间复杂度: O ( n ) O(n) O(n)
3、代码详解
#define ll int
class Solution {
stack<int> st; // (1)
int topIndex, size;
ll maxRet;
public:
int largestRectangleArea(vector<int>& heights) {
while(!st.empty()) { // (2)
st.pop();
}
maxRet = 0;
heights.push_back(-1); // (3)
size = heights.size();
for (int i = 0; i < size; ++i) {
if (st.empty() || heights[st.top()] <= heights[i]) { // (4)
st.push(i);
}
else {
while (!st.empty() && heights[st.top()] >= heights[i]) { // (5)
topIndex = st.top();
st.pop();
maxRet = max(maxRet, (ll)(i - topIndex) * (ll)heights[topIndex]);
}
st.push(topIndex); // (6)
heights[topIndex] = heights[i]; // (7)
}
}
return (int) maxRet;
}
};
-
(
1
)
(1)
(1)
stack<int>
为 c++ 中 STL 的栈模板; -
(
2
)
(2)
(2) 由于栈变量
st
是成员变量,所以可能涉及到上一组数据的缓存,所以要先清空; -
(
3
)
(3)
(3) 为了所有元素都能够出栈,需要加入个最小的不存在的高度边界,即
-1
; - ( 4 ) (4) (4) 维护一个从栈底到栈顶的单调递增栈;
- ( 5 ) (5) (5) 如果 栈顶元素 大于等于 当前元素,则一直 弹出栈顶元素 进行比较;并且计算可行的矩阵;
- ( 6 ) (6) (6) 将 当前元素 的下标继续入栈;
- ( 7 ) (7) (7) 削峰;
三、本题小知识
一般通过观察数据范围,就能够确定算法的时间复杂度。如果 ≤ 1 0 5 \le 10^5 ≤105 一般就是 O ( n ) O(n) O(n) 或者 O ( l o g 2 n ) O(log_2n) O(log2n)。