代码随想录算法训练营第64天|单调栈part03|84.柱状图中最大的矩形
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;
}
};
如上代码并不能通过leetcode,超时了,因为时间复杂度是 O ( n 2 ) O(n^2) O(n2)。
双指针解法
本题双指针的写法整体思路和42. 接雨水 (opens new window)是一致的,但要比42. 接雨水 (opens new window)难一些。
难就难在本题要记录记录每个柱子 左边第一个小于该柱子的下标,而不是左边第一个小于该柱子的高度。
所以需要循环查找,也就是下面在寻找的过程中使用了while,详细请看下面注释,整理思路在题解:42. 接雨水 (opens new window)中已经介绍了。
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
vector<int> minLeftIndex(heights.size());
vector<int> minRightIndex(heights.size());
int size = heights.size();
// 记录每个柱子 左边第一个小于该柱子的下标
minLeftIndex[0] = -1; // 注意这里初始化,防止下面while死循环
for (int i = 1; i < size; i++) {
int t = i - 1;
// 这里不是用if,而是不断向左寻找的过程
while (t >= 0 && heights[t] >= heights[i]) t = minLeftIndex[t];
minLeftIndex[i] = t;
}
// 记录每个柱子 右边第一个小于该柱子的下标
minRightIndex[size - 1] = size; // 注意这里初始化,防止下面while死循环
for (int i = size - 2; i >= 0; i--) {
int t = i + 1;
// 这里不是用if,而是不断向右寻找的过程
while (t < size && heights[t] >= heights[i]) t = minRightIndex[t];
minRightIndex[i] = t;
}
// 求和
int result = 0;
for (int i = 0; i < size; i++) {
int sum = heights[i] * (minRightIndex[i] - minLeftIndex[i] - 1);
result = max(sum, result);
}
return result;
}
};
单调栈
本地单调栈的解法和接雨水的题目是遥相呼应的。
为什么这么说呢,42. 接雨水 (opens new window)是找每个柱子左右两边第一个大于该柱子高度的柱子,而本题是找每个柱子左右两边第一个小于该柱子的柱子。
这里就涉及到了单调栈很重要的性质,就是单调栈里的顺序,是从小到大还是从大到小。
在题解42. 接雨水 (opens new window)中我讲解了接雨水的单调栈从栈头(元素从栈头弹出)到栈底的顺序应该是从小到大的顺序。
那么因为本题是要找每个柱子左右两边第一个小于该柱子的柱子,所以从栈头(元素从栈头弹出)到栈底的顺序应该是从大到小的顺序!
我来举一个例子,如图:
只有栈里从大到小的顺序,才能保证栈顶元素找到左右两边第一个小于栈顶元素的柱子。
所以本题单调栈的顺序正好与接雨水反过来。
此时大家应该可以发现其实就是栈顶和栈顶的下一个元素以及要入栈的三个元素组成了我们要求最大面积的高度和宽度
理解这一点,对单调栈就掌握的比较到位了。
除了栈内元素顺序和接雨水不同,剩下的逻辑就都差不多了,在题解42. 接雨水 (opens new window)我已经对单调栈的各个方面做了详细讲解,这里就不赘述了。
主要就是分析清楚如下三种情况:
情况一:当前遍历的元素heights[i]大于栈顶元素heights[st.top()]的情况
情况二:当前遍历的元素heights[i]等于栈顶元素heights[st.top()]的情况
情况三:当前遍历的元素heights[i]小于栈顶元素heights[st.top()]的情况
C++代码如下:
// 版本一
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
int result = 0;
stack<int> st;
heights.insert(heights.begin(), 0); // 数组头部加入元素0
heights.push_back(0); // 数组尾部加入元素0
st.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()]) { // 注意是while
int 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;
}
};
细心的录友会发现,我在 height数组上后,都加了一个元素0, 为什么这么做呢?
首先来说末尾为什么要加元素0?
如果数组本身就是升序的,例如[2,4,6,8],那么入栈之后 都是单调递减,一直都没有走 情况三 计算结果的哪一步,所以最后输出的就是0了。 如图:
那么结尾加一个0,就会让栈里的所有元素,走到情况三的逻辑。
开头为什么要加元素0?
如果数组本身是降序的,例如 [8,6,4,2],在 8 入栈后,6 开始与8 进行比较,此时我们得到 mid(8),rigt(6),但是得不到 left。
(mid、left,right 都是对应版本一里的逻辑)
因为 将 8 弹出之后,栈里没有元素了,那么为了避免空栈取值,直接跳过了计算结果的逻辑。
之后又将6 加入栈(此时8已经弹出了),然后 就是 4 与 栈口元素 8 进行比较,周而复始,那么计算的最后结果resutl就是0。 如图所示:
所以我们需要在 height数组前后各加一个元素0。
代码:
python
# 暴力解法(leetcode超时)
class Solution:
def largestRectangleArea(self, heights: List[int]) -> int:
# 从左向右遍历:以每一根柱子为主心骨(当前轮最高的参照物),迭代直到找到左侧和右侧各第一个矮一级的柱子
res = 0
for i in range(len(heights)):
left = i
right = i
# 向左侧遍历:寻找第一个矮一级的柱子
for _ in range(left, -1, -1):
if heights[left] < heights[i]:
break
left -= 1
# 向右侧遍历:寻找第一个矮一级的柱子
for _ in range(right, len(heights)):
if heights[right] < heights[i]:
break
right += 1
width = right - left - 1
height = heights[i]
res = max(res, width * height)
return res
# 双指针
class Solution:
def largestRectangleArea(self, heights: List[int]) -> int:
size = len(heights)
# 两个DP数列储存的均是下标index
min_left_index = [0] * size
min_right_index = [0] * size
result = 0
# 记录每个柱子的左侧第一个矮一级的柱子的下标
min_left_index[0] = -1 # 初始化防止while死循环
for i in range(1, size):
# 以当前柱子为主心骨,向左迭代寻找次级柱子
temp = i - 1
while temp >= 0 and heights[temp] >= heights[i]:
# 当左侧的柱子持续较高时,尝试这个高柱子自己的次级柱子(DP
temp = min_left_index[temp]
# 当找到左侧矮一级的目标柱子时
min_left_index[i] = temp
# 记录每个柱子的右侧第一个矮一级的柱子的下标
min_right_index[size-1] = size # 初始化防止while死循环
for i in range(size-2, -1, -1):
# 以当前柱子为主心骨,向右迭代寻找次级柱子
temp = i + 1
while temp < size and heights[temp] >= heights[i]:
# 当右侧的柱子持续较高时,尝试这个高柱子自己的次级柱子(DP
temp = min_right_index[temp]
# 当找到右侧矮一级的目标柱子时
min_right_index[i] = temp
for i in range(size):
area = heights[i] * (min_right_index[i] - min_left_index[i] - 1)
result = max(area, result)
return result
# 单调栈
class Solution:
def largestRectangleArea(self, heights: List[int]) -> int:
# Monotonic Stack
'''
找每个柱子左右侧的第一个高度值小于该柱子的柱子
单调栈:栈顶到栈底:从大到小(每插入一个新的小数值时,都要弹出先前的大数值)
栈顶,栈顶的下一个元素,即将入栈的元素:这三个元素组成了最大面积的高度和宽度
情况一:当前遍历的元素heights[i]大于栈顶元素的情况
情况二:当前遍历的元素heights[i]等于栈顶元素的情况
情况三:当前遍历的元素heights[i]小于栈顶元素的情况
'''
# 输入数组首尾各补上一个0(与42.接雨水不同的是,本题原首尾的两个柱子可以作为核心柱进行最大面积尝试
heights.insert(0, 0)
heights.append(0)
stack = [0]
result = 0
for i in range(1, len(heights)):
# 情况一
if heights[i] > heights[stack[-1]]:
stack.append(i)
# 情况二
elif heights[i] == heights[stack[-1]]:
stack.pop()
stack.append(i)
# 情况三
else:
# 抛出所有较高的柱子
while stack and heights[i] < heights[stack[-1]]:
# 栈顶就是中间的柱子,主心骨
mid_index = stack[-1]
stack.pop()
if stack:
left_index = stack[-1]
right_index = i
width = right_index - left_index - 1
height = heights[mid_index]
result = max(result, width * height)
stack.append(i)
return result
# 单调栈精简
class Solution:
def largestRectangleArea(self, heights: List[int]) -> int:
heights.insert(0, 0)
heights.append(0)
stack = [0]
result = 0
for i in range(1, len(heights)):
while stack and heights[i] < heights[stack[-1]]:
mid_height = heights[stack[-1]]
stack.pop()
if stack:
# area = width * height
area = (i - stack[-1] - 1) * mid_height
result = max(area, result)
stack.append(i)
return result