单调栈

这篇博客记录Leetcode刷题中遇到的单调栈问题,对于单调栈,要想明白是递增还是递减,栈顶元素代表什么,当遍历到比栈顶元素大或小的元素时,要怎么操作?

目录

A. 求下一个最大元素:

A.1 496. 下一个更大元素 I

A.1.1

A.1.2

A.2 503. 下一个更大元素 II

A.2.1

 A.2.2

A.3 739. 每日温度

 B. NGE问题的变形

B.1 907. 子数组的最小值之和

C. 找最左边小于num[i]的位置

C.1 962. 最大宽度坡

C.2 1124. 表现良好的最长时间段

D. 若干单调栈问题

D.1 402. 移掉K位数字

D.1.2 31. 下一个排列

D.2 316. 去除重复字母

D.3 456. 132模式

D.3.1

D.3.2

D.4 862. 和至少为 K 的最短子数组


A. 求下一个最大元素

(NGE: Next Greater Element):

A.1 496. 下一个更大元素 I

给定两个 没有重复元素 的数组 nums1 和 nums2 ,其中nums1 是 nums2 的子集。找到 nums1 中每个元素在 nums2 中的下一个比其大的值。

nums1 中数字 x 的下一个更大元素是指 x 在 nums2 中对应位置的右边的第一个比 x 大的元素。如果不存在,对应位置输出 -1 。

示例 1:

输入: nums1 = [4,1,2], nums2 = [1,3,4,2].
输出: [-1,3,-1]
解释:
    对于num1中的数字4,你无法在第二个数组中找到下一个更大的数字,因此输出 -1。
    对于num1中的数字1,第二个数组中数字1右边的下一个较大数字是 3。
    对于num1中的数字2,第二个数组中没有下一个更大的数字,因此输出 -1。
示例 2:

输入: nums1 = [2,4], nums2 = [1,2,3,4].
输出: [3,-1]
解释:
    对于 num1 中的数字 2 ,第二个数组中的下一个较大数字是 3 。
    对于 num1 中的数字 4 ,第二个数组中没有下一个更大的数字,因此输出 -1 。

典型的单调栈问题,虽然有两个数组,因为nums1中的元素都在nums2里,本质是找到nums2中的每个元素对应的下一个更大元素的值,之后只要建立map对应即可。 找NGE的问题,可以从前向后遍历,也可以从后向前遍历,想法有所不同。

A.1.1

从前向后遍历时,例如遍历到nums[i],如果栈顶元素(下标)对应的值比nums[i] 要小,那么说明栈顶元素对应的下标已经找到了它的下一个更大值,将它出栈,直到栈为空,或者栈顶元素(下标)对应的值大于nums[i]为止。最后将i入栈。

class Solution {
public:
    vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {
        stack<int> st;
        unordered_map<int,int> mp;
        for (int i = 0; i < nums2.size(); ++i)
        {
            while(!st.empty() && nums2[i] > nums2[st.top()])
            {
                mp[nums2[st.top()]] = nums2[i];
                st.pop();
            }
            st.push(i);
        }
        vector<int> ret(nums1.size(), -1);
        for (int i = 0; i < nums1.size(); ++i)
        {
            if(mp.count(nums1[i]) > 0) ret[i] = mp[nums1[i]];
        }
        return ret;
    }
};

A.1.2

从后向前遍历时,例如遍历到nums[j],如果nums[j]小于栈顶元素(下标)对应的值,那么说明nums[j]已经找到了它的下一个更大值,并将它压栈。否则,直到栈空或者栈顶元素大于nums[j]为止,持续出栈,最后将 j 压栈。可以出栈而不破坏问题的解的原因是,对于出栈的元素,他们本身都不大于nums[j],因此对于j前面的元素,他们的NGE不可能是这些出栈的元素。

class Solution {
public:
    vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {
        stack<int> st;
        unordered_map<int,int> mp;
        for (int j = nums2.size() - 1; j >= 0; --j)
        {
            while(!st.empty() && nums2[j] > nums2[st.top()]) st.pop();
            mp[nums2[j]] = (st.empty() ? -1 : nums2[st.top()]);
            st.push(j);
        }
        vector<int> ret(nums1.size(), -1);
        for (int i = 0; i < nums1.size(); ++i)
        {
            ret[i] = mp[nums1[i]];
        }
        return ret;
    }
};

A.2 503. 下一个更大元素 II

给定一个循环数组(最后一个元素的下一个元素是数组的第一个元素),输出每个元素的下一个更大元素。数字 x 的下一个更大的元素是按数组遍历顺序,这个数字之后的第一个比它更大的数,这意味着你应该循环地搜索它的下一个更大的数。如果不存在,则输出 -1。

示例 1:

输入: [1,2,1]
输出: [2,-1,2]
解释: 第一个 1 的下一个更大的数是 2;
数字 2 找不到下一个更大的数; 
第二个 1 的下一个最大的数需要循环搜索,结果也是 2。

A.2.1

这一题在NGE问题的基础上增加了循环,最简单的做法是,复制一份相同的数组接在它的后面,然后考察前半部分的NGE即可。

class Solution {
public:
    vector<int> nextGreaterElements(vector<int>& nums) {
        int n = nums.size();
        vector<int> twice(n * 2, 0);
        for (int i = 0; i < n; ++i)
        {
            twice[i] = twice[i+n] = nums[i];
        }
        vector<int> ret(n, -1);
        stack<int> st;
        for (int j = 2*n - 1; j >= 0; --j)
        {
            while(!st.empty() && twice[j] >= twice[st.top()]) st.pop();
            if(!st.empty() && j < n) ret[j] = twice[st.top()];
            st.push(j);
        }
        return ret;
    }
};

 A.2.2

显然,复制一份接在后面的做法是很低效的,这个操作可以用取模运算来代替。

class Solution {
public:
    vector<int> nextGreaterElements(vector<int>& nums) {
        int n = nums.size();
        vector<int> ret(n, -1);
        stack<int> st;
        for (int j = 2*n - 1; j >= 0; --j)
        {
            while(!st.empty() && nums[j%n] >= nums[st.top()%n]) st.pop();
            if(!st.empty()) ret[j%n] = nums[st.top()%n];
            st.push(j);
        }
        return ret;
    }
};

A.3 739. 每日温度

根据每日 气温 列表,请重新生成一个列表,对应位置的输出是需要再等待多久温度才会升高超过该日的天数。如果之后都不会升高,请在该位置用 0 来代替。

例如,给定一个列表 temperatures = [73, 74, 75, 71, 69, 72, 76, 73],你的输出应该是 [1, 1, 4, 2, 1, 1, 0, 0]。

提示:气温 列表长度的范围是 [1, 30000]。每个气温的值的均为华氏度,都是在 [30, 100] 范围内的整数。

同样是NGE问题,不过这次变成了求距离,那么栈中就应该保存下标。

class Solution {
public:
    vector<int> dailyTemperatures(vector<int>& T) {
        stack<int> st;
        int n = T.size();
        vector<int> ret(n, 0);
        for (int j = n - 1; j >= 0; --j)
        {
            while(!st.empty() && T[j] >= T[st.top()]) st.pop();
            if(!st.empty()) ret[j] = st.top() - j;
            st.push(j);
        }
        return ret;
    }
};

 B. NGE问题的变形

这类问题类似于NGE问题,都是找num[ i ] 的前/后一个大于/小于它的数。

B.1 907. 子数组的最小值之和

给定一个整数数组 A,找到 min(B) 的总和,其中 B 的范围为 A 的每个(连续)子数组。

由于答案可能很大,因此返回答案模 10^9 + 7。

 

示例:

输入:[3,1,2,4]
输出:17
解释:
子数组为 [3],[1],[2],[4],[3,1],[1,2],[2,4],[3,1,2],[1,2,4],[3,1,2,4]。 
最小值为 3,1,2,4,1,1,2,1,1,1,和为 17。
 

提示:

1 <= A <= 30000
1 <= A[i] <= 30000

对于 nums[i],如果它是某个子数组[ j, k ]的最小值,那么意味着 [j...i-1] 和 [i+1, k] 的值都大于等于 nums[i],i 前面有连续a个元素(a = i - j)大于等于它,i 后面有连续 b 个元素(b = k - i) 个元素大于等于它,那么以 i 为最小值的子数组个数是 a + b + ab + 1。

同时,区间端点意味着 j - 1 和 k + 1 分别是 i 前后第一个小于它的元素。所以,找 j 和 k 的问题就变成了可以用单调栈解决的问题。

class Solution {
public:
    using ull = unsigned long long;
    int sumSubarrayMins(vector<int>& A) {
        int n = A.size();
        vector<pair<ull,ull>> v(n);
        // v[i]:记录i前面连续大于它的个数和i后面连续大于等于它的个数
        stack<int> st1, st2;
        // 找 i 前面第一个小于它的位置
        for (int i = 0; i < n; ++i)
        {
            while(!st1.empty() && A[i] < A[st1.top()]) st1.pop();
            if(!st1.empty()) v[i].first = i - 1 - st1.top();
            else v[i].first = i;
            st1.push(i);
        }
        // 找 j 后面第一个小于等于它的位置
        for (int j = n - 1; j >= 0; --j)
        {
            while(!st2.empty() && A[j] <= A[st2.top()]) st2.pop();
            if(!st2.empty()) v[j].second = st2.top() - 1 - j;
            else v[j].second = n - 1 - j;
            st2.push(j);
        }
        ull ret = 0;
        for (int i = 0; i < n; ++i)
        {
            ret += A[i] * (v[i].first + v[i].second + v[i].first* v[i].second + 1);
            ret %= 1000000007;
        }
        return ret;
    }
};

C. 找最左边小于num[i]的位置

不同于NGE问题,单调栈的逻辑不容易想

C.1 962. 最大宽度坡

给定一个整数数组 A,坡是元组 (i, j),其中  i < j 且 A[i] <= A[j]。这样的坡的宽度为 j - i。

找出 A 中的坡的最大宽度,如果不存在,返回 0 。

 

示例 1:

输入:[6,0,8,2,1,5]
输出:4
解释:
最大宽度的坡为 (i, j) = (1, 5): A[1] = 0 且 A[5] = 5.
示例 2:

输入:[9,8,1,0,1,9,4,0,4,1]
输出:7
解释:
最大宽度的坡为 (i, j) = (2, 9): A[2] = 1 且 A[9] = 1.
 

提示:

2 <= A.length <= 50000
0 <= A[i] <= 50000

 对所有的 A[ j ], 找到最左边的小于等于它的位置 i, 而不是左边第一个小于等于它的位置i,因此不能用NGE的思想。

这题需要数学上证明这样一个事实:最大的宽度对应的坡底,存在于以A[0]为起点的递减序列中。假设 k 是最大的宽度对应的坡底,说明k前面的元素都要大于k,否则最大宽度还可以向前扩展。但是既然 k 前面的元素都大于k, 说明 k 一定存在于以A[0]为起点的递减序列里。

利用单调栈,找到A[0]为起点的严格递减序列 (因为要取宽度的最大值,如果有重复的元素,肯定是先出现的能使宽度更大) ,之后从右向左遍历,如果A[j] 大于栈顶元素对应的值,那么就可以更新最大宽度并出栈。小于等于A[j] 的最左边的位置就是最后一个出栈的元素。

class Solution {
public:
    int maxWidthRamp(vector<int>& A) {
        stack<int> st;
        int n = A.size();
        int ret = 0;
        for (int i = 0; i < n; ++i)
        {
            if(st.empty() || A[i] < A[st.top()]) st.push(i);
        }
        for (int j = n - 1; j >= 0; --j)
        {
            int left = -1;
            while(!st.empty() && A[j] >= A[st.top()])
            {
                left = st.top();
                st.pop();
            }
            if(left > -1) ret = max(ret, j - left);
        }
        return ret;
    }
};

C.2 1124. 表现良好的最长时间段

给你一份工作时间表 hours,上面记录着某一位员工每天的工作小时数。

我们认为当员工一天中的工作小时数大于 8 小时的时候,那么这一天就是「劳累的一天」。

所谓「表现良好的时间段」,意味在这段时间内,「劳累的天数」是严格 大于「不劳累的天数」。

请你返回「表现良好时间段」的最大长度。

 

示例 1:

输入:hours = [9,9,6,0,6,6,9]
输出:3
解释:最长的表现良好时间段是 [9,9,6]。
 

提示:

1 <= hours.length <= 10000
0 <= hours[i] <= 16

由于只有两种状态,可以把 > 8 的元素记为1,<= 8的元素记为-1,题目就转换为 [i...j]的元素和大于0,求最大的 j - i + 1。利用前缀和数组, [i...j]的元素和表示为 sum[j + 1] - sum[ i ] > 0,那么问题就转换为,求 sum[j] > sum[i] 的最大的 j - i的值,和上面962就变成了相同的题目。

class Solution {
public:
    int longestWPI(vector<int>& hours) {
        int n = hours.size();
        vector<int> v(n, 1), sum(n+1, 0);
        for (int i = 0; i < n; ++i)
        {
            if(hours[i] <= 8) v[i] = -1;
            sum[i+1] = sum[i] + v[i];
        }
        // [i...j]的和 sum[j+1] - sum[i] > 0, 即找到满足 sum[j+1] > sum[i], 使j+1 - i最大的下标j+1 和 i, 问题转换为962最大坡度问题
        stack<int> st;
        for (int i = 0; i <= n; ++i)
        {
            if(st.empty() || sum[i] < sum[st.top()]) st.push(i);
        }
        int ret = 0;
        for (int j = n; j >= 0; --j)
        {
            int last = -1;
            while(!st.empty() && sum[j] > sum[st.top()])
            {
                last = st.top();
                st.pop();
            }
            if(last > -1) ret = max(ret, j - last);
        }
        return ret;
    }
};

D. 若干单调栈问题

D.1 402. 移掉K位数字

给定一个以字符串表示的非负整数 num,移除这个数中的 k 位数字,使得剩下的数字最小。

注意:

num 的长度小于 10002 且 ≥ k。
num 不会包含任何前导零。


示例 1 :输入: num = "1432219", k = 3
输出: "1219"
解释: 移除掉三个数字 4, 3, 和 2 形成一个新的最小的数字 1219。

示例 2 :输入: num = "10200", k = 1
输出: "200"
解释: 移掉首位的 1 剩下的数字为 200. 注意输出不能有任何前导零。

示例 3 :输入: num = "10", k = 2
输出: "0"
解释: 从原数字移除所有的数字,剩余为空就是0。

 怎样删数字能使删掉后的结果最小呢?因为删除k位后数字的位数是确定的,对于两个相同位数的数字 abc... 和 xyz...,从左边开始比较每一位,先出现较小的数的那一个数是最小的。因此,应当考虑从左往右删除,被删除的数应当大于它后面的数。因此,维护单调栈,当遍历到的数小于栈顶时,并且还可以继续删除数时,就持续出栈,最后存在栈里的就是需要的结果。当然,可能整个字符串是升序,那么只需要出栈k次,相当于把末尾的k个数删除。

class Solution {
public:
    string removeKdigits(string num, int k) {
        stack<char> st;
        if(k == num.size()) return "0";
        for(char c : num)
        {
            while(k > 0 && !st.empty() && c < st.top())
            {
                st.pop();
                k--;
            }
            st.push(c);
        }
        while(k > 0 && !st.empty())
        {
            st.pop();
            k--;
        }
        string t;
        while(!st.empty())
        {
            t += st.top();
            st.pop();
        }
        string ret;
        int i = t.size() - 1;
        while(i >= 0 && t[i] == '0') i--;
        while(i >= 0) ret += t[i--];
        if(ret.empty()) return "0";
        return ret;
    }
};

和数字比大小有关的题目还有不少,例如下一个排列问题:

D.1.2 31. 下一个排列

实现获取下一个排列的函数,算法需要将给定数字序列重新排列成字典序中下一个更大的排列。

如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列)。

必须原地修改,只允许使用额外常数空间。

以下是一些例子,输入位于左侧列,其相应输出位于右侧列。
1,2,3 → 1,3,2
3,2,1 → 1,2,3
1,1,5 → 1,5,1

这题与单调栈无关,但是也比较难想。算法是:从右向左找到第一个升序对 ( i, i+1 ),然后从右向左 在[i+1, end)中找到第一个大于 num[ i ]的数 num[ j ],因为 [i+1, end)是降序序列,因此 j 是[i +1, end) 中大于 i 的最小值,将它与 i 调换,[i + 1, end)依然是降序序列,最后将[i+1, end) 逆序,就得到了下一个排列。如果没有升序对,直接逆序。

class Solution {
public:
   void nextPermutation(vector<int>& nums) {
        int i=0;
        for (i=nums.size()-2; i >= 0; -- i) { // 从后往前找到第一个相邻升序对
            if (nums[i] < nums[i+1]) break;
        }
        if (i == -1) reverse(nums.begin(),nums.end()); // 无相邻升序对,必定为非递减序列
        else {
            for (int j=nums.size()-1; j >= i+1; -- j) { // 从后往前[i+1,end)找第一个大于a[i+1]的值
                if (nums[i] < nums[j]) {
                    swap(nums[i],nums[j]); // 交换二者
                    reverse(nums.begin()+i+1,nums.end()); // 反转[i+1,end),变成升序
                    break;
                }
            }
        }
    }
};

D.2 316. 去除重复字母

给你一个仅包含小写字母的字符串,请你去除字符串中重复的字母,使得每个字母只出现一次。需保证返回结果的字典序最小(要求不能打乱其他字符的相对位置)。

 

示例 1:输入: "bcabc"
输出: "abc"


示例 2:输入: "cbacdcbc"
输出: "acdb"

 看起来和上面的删除k个数使字典序最小有些类似,不过这里要求的是删掉重复的字母。算法还是比较难想。维护单调栈,遍历字符串,如果当前字符小于栈顶元素,并且栈顶元素在后面也有出现,并且当前元素不在栈里,那么持续出栈,最后将当前字符入栈。遍历结束后栈中的元素组成字符串,再逆序,就得到了结果。

class Solution {
public:
    // 如果当前元素满足:小于栈顶、栈顶元素在后面也出现、当前元素不在栈里,那么持续出栈,最后将当前元素入栈
    string smallestSubsequence(string text) {
        vector<int> lastPosition(256, -1);
        for (int i = 0; i < text.size(); ++i)
        {
            lastPosition[text[i]] = i;
        }
        stack<char> st;
        vector<bool> hash(256, false);
        for (int i = 0; i < text.size(); ++i)
        {
            while(!st.empty() && !hash[text[i]] && text[i] < st.top() && lastPosition[st.top()] > i)
            {
                hash[st.top()] = false;
                st.pop();
            }
            if(!hash[text[i]])
            {
                st.push(text[i]);
                hash[text[i]] = true;
            }
        }
        string ret;
        while(!st.empty())
        {
            ret += st.top();
            st.pop();
        }
        reverse(ret.begin(), ret.end());
        return ret;
    }
};

D.3 456. 132模式

给定一个整数序列:a1, a2, ..., an,一个132模式的子序列 ai, aj, ak 被定义为:当 i < j < k 时,ai < ak < aj。设计一个算法,当给定有 n 个数字的序列时,验证这个序列中是否含有132模式的子序列。

注意:n 的值小于15000。

示例1:

输入: [1, 2, 3, 4]

输出: False

解释: 序列中不存在132模式的子序列。
示例 2:

输入: [3, 1, 4, 2]

输出: True

解释: 序列中有 1 个132模式的子序列: [1, 4, 2].
示例 3:

输入: [-1, 3, 2, 0]

输出: True

解释: 序列中有 3 个132模式的的子序列: [-1, 3, 2], [-1, 3, 0] 和 [-1, 2, 0].

D.3.1

最直接的想法,要找 ai < ak < aj, i < j < k, 那么对于每一个位置 k, 找到它左边第一个大于它的位置 j , 和最左边小于它的位置 i, 如果这样的 i 和 j 存在,那么就找到了这样的序列。找左边第一个大于它的位置,很容易想到用单调栈解决。而找最左边小于它的位置,就有点类似于 Part C 的两题,最大坡度问题。同样,先得到一个以 a[0] 开头的递减序列,如果 k 的左边存在比它小的元素,那一定存在于这个递减序列中。所以只要直接从左往右遍历这个递减序列即可,如果找到这个递减序列中某个元素的对应值小于k,并且位置在 k 左边第一个大于它的位置的前面,那么就找到了这样的序列。

class Solution {
public:
    // 对于位置k, 需要找到左边第一个大于它的位置j,和最左边小于它的位置i,如果 0 <= i < j, 那么说明存在序列 ai < ak < aj;
    // 找左边第一个大于它的位置,用单调栈解决;
    // 找最左边小于它的位置的方法:确定以 a[0] 开始的递减序列,如果k 的左边存在小于它的元素,那么一定在这个递减序列里,只需要遍历这个递减序列,找到第一个小于k的位置。
    bool find132pattern(vector<int>& nums) {
        stack<int> st1;
        int n = nums.size();
        // previousGreaterElement: 存放每个位置对应的前一个大于它的元素的下标
        vector<int> previousGreaterElement(n, -1);
        for (int i = 0; i < n; ++i)
        {
            while(!st1.empty() && nums[i] >= nums[st1.top()]) st1.pop();
            if(!st1.empty()) previousGreaterElement[i] = st1.top();
            st1.push(i);
        }
        // decrese: 存放从位置0开始的递减序列的下标
        vector<int> decrease;
        for (int i = 0; i < n; ++i)
        {
            if(decrease.empty() || nums[i] < nums[decrease.back()]) decrease.push_back(i);
        }
        int m = decrease.size();
        for (int j = n - 1; j >= 0; --j)
        {
            if(previousGreaterElement[j] > -1)
            {
                int i = 0;
                while(i < m && nums[decrease[i]] >= nums[j]) i++;
                if(i < m && decrease[i] < previousGreaterElement[j]) return true;
            }
        }
        return false;
    }
};

上面的做法中,找最左边小于k的元素位置这一步,需要遍历递减序列,复杂度还是很高。下面给出另一种做法。

D.3.2

要找 ai < ak < aj, i < j < k, 可以转化为固定 j , 看 j 的左边和右边是否都存在小于 aj 的数ai, ak,并且 ai < ak。

显然 ai 越小越好,所以可以先顺序遍历一遍数组,找到每个位置往左看最小值所在的位置,并且这个最小值要小于它本身。

之后就是在每个位置 j 的右边,找存在的 ak < aj, 并且 ak > ai。寻找的策略是,从右往左遍历,维护递减栈,如果当前栈顶小于等于 ai,那么持续出栈,如果最后栈不为空,并且栈顶小于 aj ,那么就找到了i j k的序列,否则将 aj 压栈。

这里要分析的是,为什么这样的策略不会丢解呢?因为是从右往左遍历j,显然右边的 ai 小于等于左边的 ai, 换句话说,j 位置出栈的元素,它们小于等于 ai, 也就一定小于等于 a(i-1),因此它们不可能成为j-1 对应的 k, 也就不需要保留它们。最后将 j 入栈, 可能成为 j-1位置对应的 k,不会破坏栈的递减性。

class Solution {
public:
    bool find132pattern(vector<int>& nums) {
        stack<int> st;
        int n = nums.size();
        // 每个位置左边最小的位置(包含自身)
        vector<int> leftmin(n, 0);
        for (int i = 1; i < n; ++i)
        {
            if(nums[i] < nums[leftmin[i-1]])  leftmin[i] = i;
            else leftmin[i] = leftmin[i-1];
        }
        for (int j = n - 1; j > 0; --j)
        {
            if(leftmin[j] == j) continue;
            while(!st.empty() && nums[st.top()] <= nums[leftmin[j]]) st.pop();
            if(!st.empty() && nums[st.top()] < nums[j]) return true;
            st.push(j);
        }
        return false;
    }
};

D.4 862. 和至少为 K 的最短子数组

 返回 A 的最短的非空连续子数组的长度,该子数组的和至少为 K 。

如果没有和至少为 K 的非空子数组,返回 -1 。

 

示例 1:

输入:A = [1], K = 1
输出:1
示例 2:

输入:A = [1,2], K = 4
输出:-1
示例 3:

输入:A = [2,-1,2], K = 3
输出:3
 

提示:

1 <= A.length <= 50000
-10 ^ 5 <= A[i] <= 10 ^ 5
1 <= K <= 10 ^ 9

这一题的思维量比较大,也可以用单调队列来做。

单调队列的做法记录在https://blog.csdn.net/chch1996/article/details/107973157#%C2%A03.%C2%A0862.%20%E5%92%8C%E8%87%B3%E5%B0%91%E4%B8%BA%20K%20%E7%9A%84%E6%9C%80%E7%9F%AD%E5%AD%90%E6%95%B0%E7%BB%84 

首先,涉及到子数组求和的问题,无一例外都要利用前缀和来优化时间复杂度,并且一个常见套路就是一边遍历一边维护之前的前缀和信息,在当前值和之前的前缀和信息里获取结果。

得到前缀和数组后,思考题目要求的值有什么特点:

求子数组的和 >= K,那么如果当前遍历到 presum[i],要找的就是左边第一个 <= presum[i] - K 的位置 j,i 和 j 的差值可能是最短的长度。在这之后,要把 presum[i] 的信息放到之前维护的前缀和信息里,并且,之前所有大于等于 presum[i] 的值都应该被舍弃,因为它们与后面的值的组合不可能优于 presum[i]。

这样,相当在左边维护了一个单调递增栈,遍历到 presum[i] 时,要找栈里最大的那个 <= presum[i] - K 的元素,以及它在原数组中的下标 j。为了快速找到这个元素,以及对应的下标,应当同时维护两个栈(或者用pair),这样找到了值就找到了下标。之后,因为是单调递增栈,可以用vector存储,使用二分法加速查找过程。

在处理完 presum[i] 可能对应的解之后,应该把栈中大于等于 presum[i] 的值全部pop,因为它们与后面的数形成的解不可能优于 presum[i]。

class Solution {
public:
    int shortestSubarray(vector<int>& A, int K) {
        int n = A.size();
        int presum[50001] = {0};
        for(int i = 0; i < n; i++) presum[i+1] = presum[i] + A[i];
        // 对每一个 presum[i], 找[0...i-1]中,满足presum[j] <= presum[i] - K 的最大的 j 值
        // 用单调栈维护[0...i-1]的前缀和, 对于presum[k]而言,前面所有的比它大的presum[l]都可以被pop,因为它们与后面的数的组合不可能优于k
        // 这样,相当于维护了一个存放presum[x]的单调递增栈,为了快速的定位 <= presum[i] - K 的最大的 j 值,还应当另外使用一个栈,存放对应的原数组下标
        vector<int> value, index;
        value.push_back(0);
        index.push_back(0);
        int ans = INT32_MAX;
        for(int i = 1; i <= n; i++)
        {
            //找最后一个 <= presum[i] - K 在 value 中的位置
            auto it = upper_bound(value.begin(), value.end(), presum[i] - K);
            if(it != value.begin())
            {
                --it;
                // value[idx] 是最后一个 <= presum[i] - K 的值,对应的 index[idx] 就是最大的满足 presum[j] <= presum[i] - K 的 j 值
                int idx = it - value.begin();
                ans = min(ans, i - index[idx]);
            }
            while(!value.empty() && value.back() >= presum[i])
            {
                value.pop_back();
                index.pop_back();
            }
            value.push_back(presum[i]);
            index.push_back(i);
        }
        return ans < INT32_MAX ? ans : -1;
    }
};

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值