单调栈及经典问题


有问题可于评论区评论,都会看,欢迎交流。

单调栈基础知识

队列:从头出,从尾部入。
单调队列:是一种队列,只是队列中元素保持递增或递减。解决滑动区间最值问题。
:堵住一头的队列。只能从尾部入,尾部出。
单调栈:栈中的元素保持递增或递减。
单调栈元素入栈时,将违反单调性的元素都弹出去,保持单调性的元素不变;
所以单调递增栈 可以用来维护元素的最近小于关系;
单调递减栈 可以用来维护元素的最近大于关系

单调栈的代码演示

#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型结构,弹栈的同时计算雨水量。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值