力扣10.26每日一题

862. 和至少为 K 的最短子数组(困难)

给你一个整数数组 nums 和一个整数 k ,找出 nums 中和至少为 k 的 最短非空子数组 ,并返回该子数组的长度。如果不存在这样的 子数组 ,返回 -1 。
子数组 是数组中 连续 的一部分。

思路很难想到并且很难理解的一道题目。题目要我们求一个区间[i, j]这个区间的和要大于等于k同时区间的长度要最小。求区间和比较容易想到用前缀和数组预处理,然后再遍历求任意2个端点的区间和依次判断,但是这个时间复杂度是O(n_2)的,明显会TLE。比较容易想到的优化手段是通过二分查找的方式固定左端点去找区间的右端点,但是此处因为数组的元素存在负数,前缀和数组不一定是递增的,不具备二段性自然也就没办法二分查找。
这题需要通过单调队列方式求解。
我们维护一个单调递增的双向队列,队列里面的元素为递增的前缀和数组,入队和出队的过程我们在遍历nums数组的时候动态维护:

预处理nums数组的前缀和数组p(长度默认为n + 1,从1开始)
建立一个deque双端队列,队列里面存放前缀和数组的下标。遍历nums数组每个元素,将其作为区间的右端点,同时我们将队列里面前端符合p[q.front()] - p[i] >= k的数取出,更新答案,同时将其在deque里面删除。注意,此处可以删除的原因是对于i后面的每个元素(假设为r),如果p[q.front()] - p[r] >= k也满足,因为此时[q.front(), r]这个区间一长度定大于[q.front(), i]这个区间,所以此时q.front和r这段区间对答案无任何贡献,我们无需重复枚举q.front(),一旦它和当前元素满足条件就更新答案将其从队列中删除(后续元素不和它继续判断)
更新完答案后,我们将队列里面大于p[i]的元素从后端弹出(为了满足单调递增的性质),然后再插入p[i]。这里需要理解下为什么可以这么做:假设我们队列里面有个元素大于p[i](假设为p[k], p[k] > p[i] && k < i),如果遍历过程中有一个值x能和p[k]组成满足条件的区间[p[k], x],那么[p[i], x]也一定能够满足条件,而同时i又大于k,那么i对答案的贡献一定比k要更大(区间长度更小),所以我们无需维护p[k],衍生的话就是无需维护队列中大于p[i]的所有元素。

class Solution {
public:
    int shortestSubarray(vector<int>& nums, int k) {
        int n = nums.size();
        vector<long long> p(n + 1);
        for (int i = 1; i <= n; ++i) {
            p[i] = p[i - 1] + nums[i - 1];
        }
        deque<int> q;
        q.push_back(0);
        int ans = INT_MAX;
        for (int i = 1; i <= n; ++i) {
            while (q.size() && p[i] - p[q.front()] >= k) {
                ans = min(ans, i - q.front());
                q.pop_front();
            }
            while (q.size() && p[q.back()] > p[i]) {
                q.pop_back();
            }
            q.push_back(i);
        }
        if (ans == INT_MAX) return -1;
        return ans;
    }
};

你可能感兴趣的:(leetcode,算法,数据结构)