文章目录
有问题可于评论区评论,都会看,欢迎交流。
单调栈基础知识
队列:从头出,从尾部入。
单调队列:是一种队列,只是队列中元素保持递增或递减。解决滑动区间最值问题。
栈:堵住一头的队列。只能从尾部入,尾部出。
单调栈:栈中的元素保持递增或递减。
单调栈元素入栈时,将违反单调性的元素都弹出去,保持单调性的元素不变;
所以单调递增栈 可以用来维护元素的最近小于关系;
单调递减栈 可以用来维护元素的最近大于关系。
单调栈的代码演示
#include <iostream>
#include <vector>
#include <stack>
#include <cstdio>
using namespace std;
void output(vector<int> &arr, const char *msg) {
printf("%s", msg);
for (auto x : arr) {
printf("%5d", x);
}
printf("\n");
return;
}
int main() {
int n;
cin >> n;
vector<int> arr(n);
stack<int> s; //单调递增栈,存储数组的下标
vector<int> pre(n), next(n); //分别存储前面和后面比某位置小的元素索引。
vector<int> ind(n); //下标数组
for (int i = 0; i < n; i++) ind[i] = i;
for (int i = 0; i < n; i++) cin >> arr[i];
for (int i = 0; i < n; i++) {
while (s.size() > 0 && arr[i] < arr[s.top()]) {
next[s.top()] = i; //对于s.top()位置的值,后面第一个比其小的值在i位置
s.pop(); //违反了单调性,弹出
}
if (s.size() == 0) pre[i] = -1;
else pre[i] = s.top(); //对于i位置元素来说,前面第一个比起小的值在s.top()位置
s.push(i);
}
while(s.size()) next[s.top()] = n, s.pop();
output(ind, "ind : ");
output(arr, "now : ");
output(pre, "pre : ");
output(next, "next : ");
}
输入为:
10
6 7 9 0 8 3 4 5 1 2
输出为:
ind : 0 1 2 3 4 5 6 7 8 9
now : 6 7 9 0 8 3 4 5 1 2
pre : -1 0 1 -1 3 3 5 6 3 8
next : 3 3 3 10 5 8 8 8 10 10
例如对于索引为4的值8而言,前面第一个比其小的元素为0,在3的位置,后面第一个比起小的元素为5,在5的位置,所以8下面的两个值pre和next分别为3和5。
总结:单调栈可以用来维护最近元素的大于或小于关系。
单调栈经典例题
1. Leetcode 155: 最小栈
题目链接
题目解析:对于pop, push,top操作用一个普通的栈即可实现。对于get_min操作,再用一个栈来维护,第二个栈的栈顶元素专门存储第一个栈的最小元素。
入栈时,若新入的元素小于等于原来的最小值,则将其也入到第二个栈;否则什么都不做;
出栈时,若要出的元素等于最小栈的栈顶元素,则将最小栈也弹出,否则什么都不做;
get_min直接输出最小栈的栈顶元素。
class MinStack {
public:
stack<int> s, min_s;
MinStack() {
}
void push(int val) {
s.push(val);
if (min_s.size() == 0 || val <= min_s.top()) {
min_s.push(val);
}
}
void pop() {
if (min_s.top() == s.top()) min_s.pop();
s.pop();
}
int top() {
return s.top();
}
int getMin() {
return min_s.top();
}
};
/**
* Your MinStack object will be instantiated and called as such:
* MinStack* obj = new MinStack();
* obj->push(val);
* obj->pop();
* int param_3 = obj->top();
* int param_4 = obj->getMin();
*/
总结:利用栈只能单边进出的特性来维护最小值。
2. Leetcode 503: 下一个更大的元素II
题目链接
题目解析:属于最近大于关系问题,所以用单调递减栈。
对于循环的特性,可以将数组入栈两遍。
class Solution {
public:
vector<int> nextGreaterElements(vector<int>& nums) {
vector<int> ret(nums.size());
stack<int> s;
for (int i = 0; i < nums.size(); i++) ret[i] = -1;
//首先初始化为-1,不能像示例代码中都入完栈后再置-1,因为是循环数组。
for (int i = 0; i < nums.size(); i++) {
while (s.size() > 0 && nums[i] > nums[s.top()]) {
ret[s.top()] = nums[i];
s.pop();
}
s.push(i);
}
//入两遍栈
for (int i = 0; i < nums.size(); i++) {
while (s.size() > 0 && nums[i] > nums[s.top()]) {
ret[s.top()] = nums[i];
s.pop();
}
s.push(i);
}
return ret;
}
};
总结:最近大于或小于关系用单调栈;循环数组处理技巧。
3. Leetcode 901: 股票价格跨度
题目链接
题目解析:要求的是某个数往前看,有多少个连续的小于等于其的数,可以等价为往前看第一个大于它的数。
所以转化为最近大于关系,用单调递减栈。
class StockSpanner {
public:
typedef pair<int, int> PII;
stack<PII> s; //同时存储下标和元素值。
int t = 0;
StockSpanner() {
s.push(PII(INT_MAX, t++));
}
int next(int price) {
while (s.size() > 0 && price >= s.top().first) {
//要严格大于关系,所以用严格递减栈,不满足严格递减的都弹出
s.pop();
}
int res = t - s.top().second;
s.push(PII(price, t++));
return res;
}
};
/**
* Your StockSpanner object will be instantiated and called as such:
* StockSpanner* obj = new StockSpanner();
* int param_1 = obj->next(price);
*/
总结:将连续的不大于问题转化为最近的大于关系问题,进一步用单调栈。
4. Leetcode 739: 每日温度
题目链接
题目解析:属于最近的大于关系问题,所以用单调递减栈。
class Solution {
public:
vector<int> dailyTemperatures(vector<int>& temperatures) {
vector <int> ret(temperatures.size());
stack<int> s; //单调递减栈
for (int i = 0; i < temperatures.size(); i++) {
while (s.size() > 0 && temperatures[i] > temperatures[s.top()]) {
ret[s.top()] = i - s.top();
s.pop();
}
s.push(i);
}
return ret;
}
};
总结:单调栈的直接应用。
以上四道题都属于单调栈的基础应用。
5. Leetcode 84: 柱状图中的最大矩形
题目链接
题目解析:对于每一个柱子来说,其所能构成的最大面积为向左右两边扩展,找到第一个低于其的柱子,中间部分的宽度乘以该柱子的高度记为最大面积。
所以题目转化为对于每一个柱子,求前面和后面第一个小于其的柱子位置。
即最近小于关系,所以用单调递增栈。
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
vector<int> l(heights.size()), r(heights.size());
stack<int> s; //单调递增栈
for (int i = 0; i < heights.size(); i++) {
l[i] = -1;
r[i] = heights.size();
}
for (int i = 0; i < heights.size(); i++) {
while (s.size() > 0 && heights[i] < heights[s.top()]) {
r[s.top()] = i;
s.pop();
}
if (s.size() > 0) {
//这里height[i]可能等于height[s.top()],因此对于i位置来说,
//找到的左边第一个小于其的柱子可能是不对的。
//但是对于s.top()找的左边第一个小于的柱子一定是对的,
//所以不影响整体答案的正确性。
//所以无需判断 二者相等时的情况。
l[i] = s.top();
}
s.push(i);
}
int ans = INT_MIN;
for (int i = 0; i < heights.size(); i++) {
ans = max(ans, heights[i] * (r[i] - l[i] - 1));
}
return ans;
}
};
总结:遍历每个元素,考虑其成为结果柱子高度的情况,然后转化为单调栈问题。
6. Leetcode 1856: 子数组最小乘积的最大值
题目链接
题目解析:和上一道题一样,同样可以遍历每个元素,考虑其成为子数组最小值的可能。
即对于每个元素,向两边分别找到第一个比其小的元素,这样就找到了该元素作为最小值对应的最长区间,二者乘积即为该元素对应的答案。
所以转化为最近小于关系的问题,用单调递增栈。
class Solution {
public:
int maxSumMinProduct(vector<int>& nums) {
vector<int> l(nums.size()), r(nums.size());
stack<int> s;
for (int i = 0; i < nums.size(); i++) {
l[i] = -1;
r[i] = nums.size();
}
for (int i = 0; i < nums.size(); i++) {
while (s.size() > 0 && nums[i] < nums[s.top()]) {
r[s.top()] = i;
s.pop();
}
if (s.size() > 0){
//这里nums[i]可能等于nums[s.top()],因此对于i位置来说,
//找到的左边第一个小于其的值可能是不对的。
//但是对于s.top()找的左边第一个小于的值一定是对的,
//所以不影响整体答案的正确性。
//所以无需判断 二者相等时的情况。
l[i] = s.top();
}
s.push(i);
}
vector<long long> sums(nums.size() + 1); //用前缀和数组快速求区间和
for (int i = 0; i < nums.size(); i++) sums[i + 1] = sums[i] + nums[i];
long long ans = 0;
for (int i = 0; i < nums.size(); i++) {
ans = max(ans, nums[i] * (sums[r[i]] - sums[l[i] + 1]));
//sums[i] 表示前i个数字的和;对应nums 下标为0~i-1 之和
//要求的是nums下标为0 ~ r[i] - 1 数字的和 - 下标为0 ~ l[i] 数字和
//所以sums的下标分别为r[i] 和 l[i] + 1
}
return ans % (long long)(1e9 + 7);
}
};
总结:1. 遍历每个元素,考虑其成为结果区间最小值的情况,然后转化为单调栈问题。 2. 用前缀和数组快速求区间和。
7. Leetcode 907: 子数组的最小值之和
题目链接
题目解析:相当于是遍历所有可能的区间,求所有区间的最小值之和。
可以转化为:首先固定区间的末尾,求所有区间的最小值之和;然后移动遍历区间的末尾。
而对于固定区间末尾,求所有合法区间的最小值之和,可以参考下图:
依次向左找到比固定节点3小的第一个元素2,假设元素2对应的固定结尾区间最小值之和已知为sum2(上图中的13+24), 则3节点对应的固定结尾区间最小值之和为 (sum2 + 3 * 4), 其中的4为2和3之间的距离。
将固定的节点进行遍历,即可求出所有区间的最小值之和。
所以关键的步骤记为对于每个节点,求出前面比其小的元素。属于最近小于关系,用单调递增栈。
class Solution {
public:
int sumSubarrayMins(vector<int>& arr) {
int n = arr.size();
vector<long long> sums(n + 1);
sums[0] = 0;
stack<int> s;
int mod_num = 1e9 + 7;
long long ans = 0;
for (int i = 0; i < n; i++) {
while (s.size() > 0 && arr[i] <= arr[s.top()]) s.pop();
int ind = s.size() > 0 ? s.top() : -1;
// sums[s.size()] = (sums[s.size() - 1] + (arr[i] * (i - ind))) % mod_num;
// ans += sums[s.size()];
//sums[i]表示i位置对应的固定区间末尾的最小值之和。
if (s.size())
sums[i] = (sums[ind] + (arr[i] * (i - ind))) % mod_num;
else
sums[i] = (arr[i] * (i - ind)) % mod_num;
ans += sums[i];
ans %= mod_num;
s.push(i);
}
return ans;
}
};
总结:区间最小值问题又称RMQ问题;固定结尾的RMQ问题可考虑单调栈。
8. Leetcode 496: 下一个更大的元素I
题目链接
题目解析:属于求最近的大于关系问题,所以可以用单调递减栈。
比较麻烦的是根据nums1中的元素,要在nums2中找到对应的位置,然后求出最近大于元素。这里可以直接用哈希表来存储最近大于关系的元素。
class Solution {
public:
vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {
stack<int> s; //单调递减栈
unordered_map<int, int> next2; //存储数组2的最近大于元素
for (int i = 0; i < nums2.size(); i++) next2[nums2[i]] = -1;
vector<int> next1(nums1.size()); //存储数组1的最近大于元素
for (int i = 0; i < nums2.size(); i++) {
while (s.size() > 0 && nums2[i] > nums2[s.top()]) {
next2[nums2[s.top()]] = nums2[i];
s.pop();
}
s.push(i);
}
for (int i = 0; i < nums1.size(); i++) {
next1[i] = next2[nums1[i]];
}
return next1;
}
};
总结:单调栈不难想到,可利用哈希表来快速查找。
9. Leetcode 456: 132模式
题目链接
题目解析:可以将数组中的每个元素都看成可能的最大值(132中的3)。然后132中的1可以等价为求3左边最小的元素。2等价为求3的右边比其小的元素的最大值。
对于遍历所有的元素作为3 和 求每个3左边的最小值都比较好实现。
对于求3的右边比其小的元素的最大值,可以对3 找到右边第一个比其大的元素,中间的元素依次弹出依次弹栈,弹出栈的最后一个元素就可以认为是3后面比其小的最大的元素。
首先看代码实现示意:
class Solution {
public:
bool find132pattern(vector<int>& nums) {
int n = nums.size();
vector<int> l(n); //存储每个位置前面元素的最小值。
stack <int> s;
l[0] = INT_MAX;
for (int i = 1; i < n; i++) l[i] = min(l[i - 1], nums[i - 1]);
//以下从后开始遍历,实现单调递减栈
for (int i = n - 1; i >= 0; i--) {
int val = nums[i];
//以下最后一个弹出的元素可以认为是i位置后面比其小的元素的最大值(val)
while (s.size() && nums[i] > s.top()) val = s.top(), s.pop();
s.push(nums[i]);
if (l[i] < nums[i] && val < nums[i] && val > l[i]) return true; //132模式
}
return false;
}
};
思考:上面代码中最后一个弹出的元素val只是当前元素3 到 第一个比他大的元素中间 所有元素的最大值。并不是3的后面所有比其小的元素的最大值。但是上述代码并不影响最终答案的正确性。
因为假设3后面第一个比当前元素3大的元素记为val_max, 通过弹栈弹出的最后一个元素为val, 而在val_3的后面还存在一个元素val_2比val大,比当前元素3小,则
l
[
i
]
,
v
a
l
_
m
a
x
,
v
a
l
_
2
l[i], val\_max, val\_2
l[i],val_max,val_2 又构成了一个132模式的子序列, 而这种情况再以val_max为当前元素时一定会遍历到,所以一定不会错过答案。如下图所示:
总结:将实际问题等价为其他子问题,再对子问题通过某种“不等价”的方式求解,也能得到正确答案。本题的精髓不在代码算法本身,而在于需要理解算法为什么是正确的。
10. Leetcode 42: 接雨水
题目链接
题目解析:什么样的情况下可以存到雨水?V型结构。而单调栈就是一个天然的寻找V型结构的数据结构。
首先用单调递减栈可以找出某个数两边比其大的元素。
然后弹栈时,用弹出栈元素的两边比其大的元素和当前元素作差,取最小值 再乘以 区间宽度,即为此次弹栈元素对应的雨水量。
class Solution {
public:
int trap(vector<int>& height) {
stack<int> s;
int ans = 0;
for (int i = 0; i < height.size(); i++) {
while (s.size() > 0 && height[i] > height[s.top()]) {
int now = s.top();
s.pop();
if (s.size() == 0) continue;
int a = height[i] - height[now];
int b = height[s.top()] - height[now];
ans += (min(a, b) * (i - s.top() - 1));
}
s.push(i);
}
return ans;
}
};
总结:寻找V型结构,弹栈的同时计算雨水量。