力扣 每日一题 862. 和至少为 K 的最短子数组【难度:困难,rating: 2306】(前缀和+单调队列)

题目链接

https://leetcode.cn/problems/shortest-subarray-with-sum-at-least-k/

题目来源于:第91场周赛 Q4 rating: 2306

思路(简单版本:假如数组不包含负数)

我们首先做一下这题的简单版本【209. 长度最小的子数组】。

假如数组中不包括负数,全部为正整数。那么可以用双端队列(deque)直接维护前缀和(前缀和用于求指定左右端点的区间和)的单调递增区间,队列中的元素存储的是数组的下标。

具体做法是:遍历整个数组,每次先把队首弹出直到不满足区间和>=target的条件,同时记录满足区间和>=target的区间长度(队首到队尾的距离),然后加入当前下标到队尾。

class Solution {
    static const int N=1e5+10;
    deque<int> q;
    int sum[N];
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int n=nums.size();
        sum[0]=0;
        for(int i=1;i<=n;i++){
            sum[i]=sum[i-1]+nums[i-1];
        }
        int mi=N;
        for(int i=0;i<=n;i++){
            while(!q.empty()&&sum[i]-sum[q.front()]>=target){
                // [q.front()+1,i]区间和>=target
                mi=min(mi,i-q.front());
                q.pop_front();
            }
            q.push_back(i);
        }
        return mi==N?0:mi;
    }
};

这一过程可以简化为双指针,不用前缀和数组sum[]以及队列deque,从而降低空间复杂度到O(1),但时间复杂度仍为O(n)。

思路(本题:数组包含负数)

这题麻烦的地方在于数组中包含负数,使得前缀和不再单调递增。但我们可以想办法维护放入队列的下标 i 对应的前缀和 sum[i] 依然单调递增。

为了做到这一点,考虑在加入当前下标 i 到队列之前,弹出不影响后续答案的元素。

  • 结论:假设 sum[i] 的前面有一些 sum[j] (j < i),满足 s u m [ j ] > = s u m [ i ] sum[j]>=sum[i] sum[j]>=sum[i],那么这些 j 被弹出并不会影响答案。
  • 证明:因为后续在计算满足条件的最小区间长度时,这些 i,j 都是作为区间的左端点,假设右端点为 r,则 s u m [ r ] − s u m [ i ] > = s u m [ r ] − s u m [ j ] sum[r]-sum[i]>=sum[r]-sum[j] sum[r]sum[i]>=sum[r]sum[j]。那么如果 s u m [ r ] − s u m [ j ] > = k sum[r]-sum[j]>=k sum[r]sum[j]>=k 则一定有 s u m [ r ] − s u m [ i ] > = k sum[r]-sum[i]>=k sum[r]sum[i]>=k。这也就意味着,那些更靠前的 j 可以被 i 替代,i 不仅更靠后,能使得区间更短,而且还能使得区间和更大,所以就可以放心的弹出 j 了。

具体到代码实现中,就是弹出sum[q.back()]>=sum[i]的队尾元素来维护单调队列。画一个例子方便理解:
在这里插入图片描述

上图中 i=2 时,将会弹出大于sum[2](即47)的队尾元素84。此时队列中元素为{0, 2},对应的前缀和为{0, 47}。

代码

typedef long long ll;
class Solution {
    static const int N=1e5+10;
    deque<int> q;
    ll sum[N];
public:
    int shortestSubarray(vector<int>& nums, int k) {
        int n=nums.size();
        sum[0]=0;
        for(int i=1;i<=n;i++){
            sum[i]=sum[i-1]+nums[i-1];
        }
        int mi=N;
        for(int r=0;r<=n;r++){
            while(!q.empty()&&sum[r]-sum[q.front()]>=k){
                // 满足题目要求 [q.front()+1,r]区间和>=k
                // 弹出队首,使得区间大小尽量缩短
                mi=min(mi,r-q.front());
                q.pop_front();
            }
            while(!q.empty()&&sum[q.back()]>=sum[r]){
                // 维护单调性,弹出大于等于当前元素的所有队尾
                // 这样操作之后就能让[q.front,q.back]单调递增,那么在队尾之后再加入一个当前元素也单调递增
                q.pop_back();
            }
            q.push_back(r);
        }
        return mi==N?-1:mi;
    }
};

/*
[84,-37,32,40,95]
167
ans: 3
*/
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

nefu-ljw

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值