单调栈最经典的解决场景是:给定一个数组,求每个位置左右两侧距离最近的比当前位置上的数小
的数(或比当前位置大的数)
求左右两侧小的数:
1.无重复值:
提供一个栈(最好用数组模拟),只要要压入的数a大于栈顶的元素,就压入栈;反之,就弹出栈顶元素,那么栈顶元素b左侧小数为弹出后栈顶的元素,右侧比b小的数就是a;如果a大于此时栈顶的元素,就压入栈,反之则继续循环重复上述操作直至栈为空
#include <iostream>
#include <vector>
using namespace std;
static int maxnum = 1000001;
static vector<vector<int>>arr(maxnum, vector<int>(2, 0));
static vector<int>nums(maxnum, 0);
int main() {
int n;
cin >> n;
for (int i = 0; i < n; i++)
cin >> nums[i];
vector<int>stack(n + 5, 0);
int top = 0;
for (int i = 0; i < n; i++) {
while (top > 0 && nums[stack[top - 1]] > nums[i]) {
int cur = stack[--top];
arr[cur][0] = top > 0 ? stack[top - 1] : -1;
arr[cur][1] = i;
}
stack[top++] = i;
}
while (top > 0) {
int cur = stack[--top];
arr[cur][0] = top > 0 ? stack[top - 1] : -1;
arr[cur][1] = -1;
}
for (int i = 0; i < n; i++)
cout << arr[i][0] << " " << arr[i][1] << endl;
return 0;
}
当数组遍历完时,栈里还有一些元素没有结算,需要单独把这些元素结算;这些元素既然没有结
算,说明他们右侧没有比他们小的数,标为-1
2.有重复值:左右两侧最近的小数
// 单调栈求每个位置左右两侧,离当前位置最近、且值严格小于的位置
// 给定一个可能含有重复值的数组 arr
// 找到每一个 i 位置左边和右边离 i 位置最近且值比 arr[i] 小的位置
// 返回所有位置相应的信息。
// 输入描述:
// 第一行输入一个数字 n,表示数组 arr 的长度。
// 以下一行输入 n 个数字,表示数组的值
// 输出描述:
// 输出n行,每行两个数字 L 和 R,如果不存在,则值为 -1,下标从 0 开始。
// 测试链接 : https://www.nowcoder.com/practice/2a2c00e7a88a498693568cef63a4b7bb
// 请同学们务必参考如下代码中关于输入、输出的处理
// 这是输入输出处理效率很高的写法
// 提交以下的code,提交时请把类名改成"Main",可以直接通过
#include <iostream>
#include <vector>
using namespace std;
static int maxnum = 1000001;
static vector<vector<int>>arr(maxnum, vector<int>(2, 0));
static vector<int>nums(maxnum, 0);
int main() {
int n;
cin >> n;
for (int i = 0; i < n; i++)
cin >> nums[i];
vector<int>stack(n + 5, 0);
int top = 0;
for (int i = 0; i < n; i++) {
while (top > 0 && nums[stack[top - 1]] >= nums[i]) {
int cur = stack[--top];
arr[cur][0] = top > 0 ? stack[top - 1] : -1;
arr[cur][1] = i;
}
stack[top++] = i;
}
while (top > 0) {//结算栈里剩下的
int cur = stack[--top];
arr[cur][0] = top > 0 ? stack[top - 1] : -1;
arr[cur][1] = -1;
}
for (int i = n - 2; i >= 0; i--) {更正右侧的的最小数
if (arr[i][1] != -1 && nums[i] == nums[arr[i][1]]) {
arr[i][1] = arr[arr[i][1]][1];
}
}
for (int i = 0; i < n; i++)
cout << arr[i][0] << " " << arr[i][1] << endl;
return 0;
}
// 64 位输出请用 printf("%lld")
若此数组有重复值,当遍历到一个数A时,此时的数和栈顶的B数值相等,该怎么处理呢?当我们
遍历A到时,就把B弹出,将B的右侧的小数记为A;遍历完数组的元素并把栈里的元素全部结算
时,再去更正重复的元素:从右向左遍历,若遍历到一个数的数值和他右侧的小数数值相同,就需
要更正,更正为其右侧小数的右侧小数
时间复杂度:每个元素出栈、入栈各一次,所以时间复杂度为O(N)
每日温度 求右侧距离最近的大数
class Solution {
public:
vector<int> dailyTemperatures(vector<int>& temperatures) {
vector<int> ans(temperatures.size(), 0);
vector<int> stack(temperatures.size() + 2, 0);
int top = 0;
for (int i = 0; i < temperatures.size(); i++) {
while (top > 0 && temperatures[stack[top - 1]] < temperatures[i]) {
int cur = stack[--top];
ans[cur] = i - cur;
}
stack[top++] = i;
}
while (top > 0) {
int cur = stack[--top];
ans[cur] = 0;
}
return ans;
}
};
此题就是模板题,只不过是求左右两侧的大数,所以栈里要"小压大",然后结算栈里剩下的元
素,最后更正相等的元素。但此题可以不用弹出相等的情况,我们先分析一下模板题,对于相等的
元素,如果弹出,会影响其右侧的答案;如果压入,会影响其左侧的答案。但对于此题,我们只需
要求其右侧的大数,所以对于相等的情况,压入等待大数来把相等的数一起结算
class Solution {
public:
int mod = 1000000007;
int sumSubarrayMins(vector<int>& arr) {
vector<int> stack(arr.size() + 2, 0);
int top = 0;
int ans = 0;
for (int i = 0; i < arr.size(); i++) {
while (top > 0 && arr[stack[top - 1]] >= arr[i]) {
int cur = stack[--top];
int left = top > 0 ? stack[top - 1] : -1;
ans = (ans % mod + (cur - left) * (i - cur) * arr[cur] % mod) %
mod;
}
stack[top++] = i;
}
while (top > 0) {
int cur = stack[--top];
int left = top > 0 ? stack[top - 1] : -1;
ans = (ans % mod +
(cur - left) * (arr.size() - cur) * arr[cur] % mod) %
mod;
}
return ans;
}
};
首先分析此题,对于一个子数组的最小值,其实就是在原数组中任取一个值,找到其左右两侧的小数,那么两个小数开区间内只要包含此数的子数组的最小值都是这个数。那么大体的思路就有了:找出左右两侧的小数,那么经过此数的子数组有(cur-left)*(i-cur)个,left为左侧小数的位置,i为右侧小数的位置(经过次数的子数组,区间开头的有cur-left个数,结尾的有i-cur个数),子数组的个数乘以此数的数值就把这一群子数组的最小值的和求出了
那么关键的问题来了,对于相等的数该怎么处理?对于相等的数B,我还是和上述的处理过程一致:把这个数A弹出结算答案,但这样以相等的数B结尾的子数组的最小值也是A啊,那么不就缺失了这一部分子数组的答案了吗?其实没关系,在后面结算B的答案时,就会把缺失的这部分给补上
最后注意答案取余的问题
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
int ans = 0;
vector<int> stack(heights.size() + 2, 0);
int top = 0;
for (int i = 0; i < heights.size(); i++) {
while (top > 0 && heights[stack[top - 1]] >= heights[i]) {
int cur = stack[--top];
int left = top > 0 ? stack[top - 1] : -1;
int width = i-left-1;
ans = max(ans, width * heights[cur]);
}
stack[top++] = i;
}
while (top > 0) {
int cur = stack[--top];
int left = top > 0 ? stack[top - 1] : -1;
int width = heights.size()-left- 1;
ans = max(ans, width * heights[cur]);
}
return ans;
}
};
要求面积最大的矩形,可以以每一个柱形为长,求出左右两侧的小数,那么在两个小数的开区间内的矩形面积就可以求出。那么关于相等的数处理情况呢?如果A遇到相等的数B,可以将A结算答案,虽然以A为长的矩形可能可以继续向B的右边扩张,但当B结算时,就会把正确的最大面积结算掉,不影响最大值;也可以不结算相等的数,那么B结算出的答案不是正确的,但A结算的答案是正确的
class Solution {
public:
int maximalRectangle(vector<vector<char>>& matrix) {
vector<int> rem(matrix[0].size(), 0);
vector<int> stack(matrix[0].size() + 2, 0);
int top = 0;
int ans = 0;
for (auto i : matrix) {
for (int j = 0; j < i.size(); j++) {//压缩数组,求每个柱体的高
if (i[j] == '1')
rem[j] += 1;
else
rem[j] = 0;
}
for (int j = 0; j < matrix[0].size(); j++) {
while (top > 0 && rem[stack[top - 1]] >= rem[j]) {
int cur = stack[--top];
int left = top > 0 ? stack[top - 1] : -1;
int width = j - left - 1;
ans = max(ans, width * rem[cur]);
}
stack[top++] = j;
}
while (top > 0) {
int cur = stack[--top];
int left = top > 0 ? stack[top - 1] : -1;
int width = matrix[0].size() - left - 1;
ans = max(ans, width * rem[cur]);
}
}
return ans;
}
};
此题的难点在于处理二维数组,我们可以采取压缩数组的方式来求出以每一行为底中最大矩形的面积
以上图为例,以每行为底的每个柱体的高:
以第0行为底:1 1 0 1
以第1行为底:2 0 1 2
······
每次都求出柱体的高,然后和"柱状图中的最大矩形"的解法一样