哎~来到了天津卫,是嘛也没学会,学会了单调栈,您看这对不对!
今天来做一道单调栈的题目:
题目链接:最大矩形面积问题
题目:
小S最近在分析一个数组 h1,h2,...,hN,数组的每个元素代表某种高度。小S对这些高度感兴趣的是,当我们选取任意 k 个相邻元素时,如何计算它们所能形成的最大矩形面积。
对于 k 个相邻的元素,我们定义其矩形的最大面积为: R(k)=k×min(h[i],h[i+1],...,h[i+k−1])
即,R(k) 的值为这 k 个相邻元素中的最小值乘以 k。现在,小S希望你能帮他找出对于任意 k,R(k) 的最大值。
思路分析
暴力解法
对于每个 i,我们可以计算从柱子 i 开始的所有连续区间的最小高度,进而计算矩形面积,或者计算以柱子 i 为最小高度的区间最大宽度。但是这样的暴力做法会导致 O(n^2) 的时间复杂度,不适合大规模输入。
优化:单调栈
为了更高效地计算矩形面积,我们使用单调栈来维护高度的索引,从而快速找到每个柱子的左右边界。
单调栈的核心思想是:
-
栈内维护一个单调递增的高度索引序列。
-
遇到破坏递增性的位置时,弹出栈顶元素并计算矩形面积。
- 以弹出的柱子为高度,确定其矩形的宽度范围(即左右边界)。
- 更新当前的最大矩形面积。
为什么使用单调栈?
-
快速找到左右边界:
- 通过栈顶元素与当前元素的索引关系,快速确定柱子 h[i] 能向左右扩展的最大宽度。
-
高效处理每个柱子:
- 每个柱子最多进栈一次、出栈一次,时间复杂度为 O(n)。
代码实现
以下是基于Java的单调栈实现:
public static int solution(int n, int[] array) {
// Edit your code here
// 使用栈来存储柱子的索引
Stack<Integer> stack = new Stack<>();
int maxArea = 0;
// 遍历每个柱子的高度
for (int i = 0; i < n; i++) {
// 当栈不为空,且当前柱子高度小于等于栈顶柱子高度时,弹出栈顶元素并计算面积
while (!stack.isEmpty() && array[stack.peek()] >= array[i]) {
int height = array[stack.pop()];
int width = stack.isEmpty() ? i : i - stack.peek() - 1;
maxArea = Math.max(maxArea, height * width);
}
// 将当前柱子的索引压入栈
stack.push(i);
}
// 处理栈中剩余的柱子
while (!stack.isEmpty()) {
int height = array[stack.pop()];
int width = stack.isEmpty() ? n : n - stack.peek() - 1;
maxArea = Math.max(maxArea, height * width);
}
return maxArea;
}
代码详解
1. 数据结构与变量初始化
Stack<Integer> stack = new Stack<>();
int maxArea = 0;
stack:存储柱子的索引,保证栈内的柱子高度单调递增。maxArea:记录目前为止找到的最大矩形面积。
2. 遍历柱子高度
for (int i = 0; i < n; i++) {
while (!stack.isEmpty() && array[stack.peek()] >= array[i]) {
int height = array[stack.pop()];
int width = stack.isEmpty() ? i : i - stack.peek() - 1;
maxArea = Math.max(maxArea, height * width);
}
stack.push(i);
}
-
逻辑:
-
当前柱子 h[i] 高度小于栈顶柱子高度时:
-
弹出栈顶柱子,计算以该柱子高度为矩形的最大面积。
-
宽度的计算:
- 如果栈为空,说明弹出的柱子能向左扩展到边界 0。
- 否则宽度是 i - stack.peek() - 1。
-
-
将当前柱子的索引压入栈。
-
3. 处理剩余柱子
while (!stack.isEmpty()) {
int height = array[stack.pop()];
int width = stack.isEmpty() ? n : n - stack.peek() - 1;
maxArea = Math.max(maxArea, height * width);
}
-
逻辑:
- 遍历完成后,栈中可能还有柱子未处理。
- 对每个柱子计算矩形面积,宽度向右扩展到数组末尾 n。
4. 返回结果
return maxArea;
返回遍历过程中找到的最大矩形面积。
复杂度分析
-
时间复杂度: O(n)
- 每个柱子最多进栈一次、出栈一次。
-
空间复杂度: O(n)
- 栈在最坏情况下存储所有柱子的索引。
扩展题目
类似的单调栈经典题目:接雨水
听说字节跳动公司随便拉一个扫地大爷都会做这道题。
这道题有多种做法,这里讲解一下单调栈做法。
题目描述
给定
n个非负整数表示每个宽度为1的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
解题思路
我们用单调栈的方式解决此问题,通过维护柱子索引的栈来计算每个低洼处可能接到的雨水量。
为什么选择单调栈?
- 单调栈能有效处理「左边最高」和「右边最高」的问题。
- 栈中的柱子会按照高度递减存储(单调递减栈)。
- 每次遇到一个比栈顶柱子高的柱子时,可以计算当前的低洼处能接的水量。
单调栈解决方法
核心思想
-
遍历柱子:
-
当前柱子高度 h[i] 小于等于栈顶柱子高度时,直接入栈。
-
当前柱子高度 h[i] 大于栈顶柱子高度时,弹出栈顶柱子并计算雨水量:
- 低洼处: 栈顶柱子高度。
- 左右边界: 栈顶柱子的下一个元素是左边界,当前柱子是右边界。
- 宽度: 右边界与左边界之间的距离。
-
代码实现
public static int trap(int[] height) {
Stack<Integer> stack = new Stack<>();
int water = 0;
for (int i = 0; i < height.length; i++) {
// 当前柱子高度大于栈顶柱子时,计算雨水量
while (!stack.isEmpty() && height[i] > height[stack.peek()]) {
int bottom = stack.pop(); // 弹出栈顶柱子(低洼处)
if (stack.isEmpty()) {
break; // 如果栈空,则没有左边界,跳过
}
int left = stack.peek(); // 左边界索引
int width = i - left - 1; // 宽度:右边界 - 左边界 - 1
int boundedHeight = Math.min(height[left], height[i]) - height[bottom]; // 高度差
water += width * boundedHeight; // 雨水量 = 宽度 × 高度差
}
stack.push(i); // 将当前柱子的索引入栈
}
return water;
}
代码详解
-
遇到低洼:
- 当前柱子高度 h[i] 小于等于栈顶柱子时,将当前柱子的索引压入栈。
- 栈内维护一个单调递减的柱子索引序列。
-
遇到边界:
-
当前柱子高度 h[i] 大于栈顶柱子时,弹出栈顶柱子,并尝试计算当前低洼处的雨水量:
-
弹出的栈顶柱子是低洼处的底部。
-
栈顶的下一个元素是左边界,当前柱子是右边界。
-
雨水面积计算:
- 宽度:i - left - 1。
- 高度:min(h[left], h[i]) - h[bottom]。
- 总雨水:宽度 × 高度。
-
-
-
当前柱子入栈:
- 每次弹出完栈顶后,当前柱子索引 i 都需要压入栈,作为后续雨水的左边界。
返回累加的雨水量。
复杂度分析
-
时间复杂度: O(n)
- 每个柱子最多进栈一次、出栈一次,因此整体复杂度为线性。
-
空间复杂度: O(n)
- 使用了栈来存储柱子的索引。
你学废了吗?