单调栈——求最近的最值元素

问题引入:

        找出一段序列的每个数的右边距离该数最近的最大值。

朴素思路:

        普通的做法两层循环(类似于双指针算法,找到了就break),但是这样写的话效率就很低了。

模拟过程:

        现给出数组:1 4 3 5 5 2 3 6

        假设从右往左思考,当已经放入2 3 6时,下一个数是5,只要5存在,就一定用不到 2 3 了。这个时候当前维护的数组就可以从 5 2 3 6 变为5 6了。后面的过程同理。

     对于这样的一个结构:首先我们可以发现:后进入的数会最先出去,先进入的数最后出去,这是栈的结构。同时也要满足栈内元素是单调的:即要么一直增大要么一直减小。

        这就是我们所说的单调栈了。

单调栈的套路:

        一般用单调栈都是求数组中某个元素,离这个元素最近的左边(上一个)/右边(下一个)最大值/最小值。这里就要考虑道一个问题:数组遍历的问题。有时候从前往后遍历,有时候从后往前遍历。

        这里给出一个经验写法:如果求的是该元素最近的左边(上一个)的最值:就从前往后循环,将栈底向左,栈顶向右。如果求的是该元素最近的右边(下一个)的最值:就从后往前循环,将栈底向右,栈顶向左。

模板:
	for (int i = 0; i < vec.size(); i++)
	{
		while (st.size() && vec[st.top()] >= vec[i]) //非空且需要弹出时
			st.pop();
        // 具体逻辑
		if (st.size()) cout << "左边距离" << vec[i] << "最近的最小值是" << vec[st.top()] << endl;
		else cout << "左边没有比" << vec[i] << "更小的数" << endl;
		st.push(i); //  弹入
	}
 两道例题:

503. 下一个更大元素 II

        由于是下一个更大元素,也就是求当前元素的右边的最大值,所以我们遍历肯定是从后往前的。

        这里还要注意:循环数组的话我们可以做一些初始化操作:再复制一遍数组加到尾上就行了。但是填充数据的时候还是要求i<n才可以填充。

class Solution {
public:
    stack<int> st;
    vector<int> nextGreaterElements(vector<int>& nums) {
        int n = nums.size();
        vector<int> vec(n,-1);
        for(int i = 0;i<n;i++)
            nums.push_back(nums[i]);
        int m = nums.size();
        for(int i = m-1;i>=0;i--)
        {
            while(st.size() && nums[st.top()]<=nums[i])
                st.pop();
            if(st.size() && i<n)
                vec[i] = nums[st.top()];
            st.push(i);
        }
        return vec;
    }
};

907. 子数组的最小值之和

        这题难度大一些:如果我们一个一个去枚举子集然后去更新最小值,是O(n^2)的复杂度。那我们就换个角度去思考:思考当前这个数对答案有多少贡献,然后遍历一边数组就可以了。这样的复杂度就是O(n)的。

        假设现在数组是 arr = {1,4,2,3,1},其中2是最小值的子集有{2},{2,3},{4,2},{4,2,3},所以2的贡献值是2*4 = 8.

        2如果要满足在当前子集中最小,则向左向右扩展时不能遇到1,下标满足为[1,3](闭区间),也可以写成(0,4)(闭区间)。

        记 L = 0,R = 4,当前下标为i。则a[i]的贡献为: a[i] * (R-i)*(i-L).

         若arr=[1,2,4,2,3,1]呢?这样对2计算就会有重复。我们把右边界修改为小于等于a[i]的下标就可以了。 第一个2:(0,3),第二个2:(0,5)

        现在的问题就是如何计算出对于a[i]的上一个严格小于a[i]元素位置,和下一个小于等于a[i]元素位置了。

        使用两遍单调栈即可。

        注意: 在计算贡献的时候,如果本身也算进去的话,下标从-1开始,到n结束。

class Solution {
public:
    const int MOD = 1e9+7;
    int sumSubarrayMins(vector<int>& arr) {
        int n = arr.size();
        vector<int> left(n,-1);
        stack<int> st;
        for(int i = 0;i<n;i++)
        {
            while(!st.empty() && arr[st.top()] >= arr[i])
                st.pop();
            if(!st.empty()) left[i] = st.top();
            st.push(i);
        }
        vector<int> right(n,n);
        while(!st.empty()) st.pop();
        for (int i = n - 1; i >= 0; --i) {
            while (!st.empty() && arr[st.top()] > arr[i])
                st.pop(); 
            if (!st.empty()) right[i] = st.top();
            st.push(i);
        }
        long ans = 0L;
        for (int i = 0; i < n; ++i)
            ans += (long) arr[i] * (i - left[i]) * (right[i] - i); // 累加贡献
        return ans % MOD;


    }
};

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值