Java&C++题解与拓展——leetcode862.和至少为K的最短子数组【么的新知识】

98 阅读3分钟
每日一题做题记录,参考官方和三叶的题解

题目要求

image.png

思路:前缀和+单调队列

  • 连续子区间立即推前缀和,然后要找最短和最大的子区间用单调队列比较方便;
  • 某个子区间的和就是两个前缀和sum[i]sum[i]的差,即i=lrnums[i]=sum[r+1]sum[l]\sum^r_{i=l}nums[i]=sum[r+1]-sum[l]
  • 然后利用一个双端队列【后续操作推定】来遍历计算每个子区间的和【每两个前缀和之间的差】:
    • 逐个遍历计算合法的子区间,为降低复杂度需将冗余队头、队尾元素去除,那么什么是冗余的呢;
    • 对于队头元素,若sumcursumhead>=ksum_{cur}-sum_{head}>=k,那么curcurheadhead两个就组成了最短的合法子数组,curcur后续元素都无法再构成一个更短的数组,所以可以将当前的队头弹出;
    • 对于队尾元素,若sumcur<sumtailsum_{cur}<sum_{tail}【此时curcur还未入队】,那么后续元素xx若满足xsumtail>=kx-sum_{tail}>=k,那么也必然满足xsumcur>=kx-sum_{cur}>=k,显然curcur构成的子数组更短,所以队尾可以弹出;
  • 过程中维护最小的结果,同时维护待遍历的双端队列。

Java

两次循环

  • 分开计算前缀和与双端队列
class Solution {
    public int shortestSubarray(int[] nums, int k) {
        int n  = nums.length, res = n + 1;
        // 前缀和
        var sum = new long[n + 1];
        for (int i = 0; i < n; i++) 
            sum[i + 1] = sum[i] + nums[i];
            
        var dq = new ArrayDeque<Integer>();
        for (int i = 0; i <= n; i++) {
            // 队头多余
            while (!dq.isEmpty() && sum[i] - sum[dq.peekFirst()] >= k)
                res = Math.min(res, i - dq.pollFirst());
            // 队尾波动
            while (!dq.isEmpty() && sum[dq.peekLast()] >= sum[i])
                dq.pollLast();
            dq.addLast(i);
        }
        return res > n ? -1 : res;
    }
}

一次循环

  • 同时计算前缀和与待遍历元素,将前缀和作为双端队列的一部分
class Solution {
    public int shortestSubarray(int[] nums, int k) {
        int res = Integer.MAX_VALUE;
        var dq = new ArrayDeque<Pair<Long, Integer>>();
        dq.addLast(new Pair<>(0L, -1));
        long cur = 0L;
        for (int i = 0; i < nums.length; i++) {
            cur += nums[i]; // 前缀和
            // 队头多余
            while (!dq.isEmpty() && cur - dq.peekFirst().getKey() >= k)
                res = Math.min(res, i - dq.pollFirst().getValue());
            // 队尾波动
            while (!dq.isEmpty() && dq.peekLast().getKey() >= cur)
                dq.pollLast();
            dq.addLast(new Pair<>(cur, i));
        }
        return res == Integer.MAX_VALUE ? -1 : res;
    }
}
  • 时间复杂度:O(n)O(n)
  • 空间复杂度:O(n)O(n)

var类型

  • 学习参考链接
  • 可以说是一个用于简化代码的类型,定义双端队列这种结构时就不需要两边都写一遍了。
  • 它可以根据后面的赋值推断类型,也就意味着后面必须有初值。

C++

两次循环

class Solution {
public:
    int shortestSubarray(vector<int>& nums, int k) {
        int n  = nums.size(), res = n + 1;
        // 前缀和
        long sum[n + 1];
        sum[0] = 0L;
        for (int i = 0; i < n; i++) 
            sum[i + 1] = sum[i] + nums[i];
            
        deque<int> dq;
        for (int i = 0; i <= n; i++) {
            // 队头多余
            while (!dq.empty() && sum[i] - sum[dq.front()] >= k) {
                res = min(res, i - dq.front());
                dq.pop_front();
            }                
            // 队尾波动
            while (!dq.empty() && sum[dq.back()] >= sum[i]) 
                dq.pop_back();
            dq.push_back(i);
        }
        return res > n ? -1 : res;
    }
};

一次循环

class Solution {
public:
    int shortestSubarray(vector<int>& nums, int k) {
        int res = INT_MAX;
        deque<pair<long, int>> dq;
        dq.emplace_back(0L, -1);
        long cur = 0L;
        for (int i = 0; i < nums.size(); i++) {
            cur += nums[i]; // 前缀和
            // 队头多余
            while (!dq.empty() && cur - dq.front().first >= k) {
                res = min(res, i - dq.front().second);
                dq.pop_front();
            }                
            // 队尾波动
            while (!dq.empty() && dq.back().first >= cur)
                dq.pop_back();
            dq.emplace_back(cur, i);
        }
        return res == INT_MAX ? -1 : res;
    }
};
  • 时间复杂度:O(n)O(n)
  • 空间复杂度:O(n)O(n)

Rust

两次循环

impl Solution {
    pub fn shortest_subarray(nums: Vec<i32>, k: i32) -> i32 {
        let n = nums.len();
        let mut res = n + 1;
        // 前缀和
        let mut sum = vec![0; n + 1];
        for i in 0..n {
            sum[i + 1] = sum[i] + nums[i] as i64;
        }
        let mut dq = std::collections::VecDeque::new();
        for i in 0..=n {
            // 队头多余
            while let Some(&cur) = dq.front() {
                if sum[i] - sum[cur] >= k as i64 {
                    res = res.min(i - cur);
                    dq.pop_front();
                }
                else {
                    break;
                }
            }
            // 队尾波动
            while let Some(&cur) = dq.back() {
                if sum[cur] >= sum[i] {
                    dq.pop_back();
                }
                else {
                    break;
                }
            }
            dq.push_back(i);
        }
        if res == n + 1 {
            -1
        }
        else {
            res as i32
        }
    }
}
  • 时间复杂度:O(n)O(n)
  • 空间复杂度:O(n)O(n)

总结

  • 今天三叶姐姐的方法太复杂了、就短暂地变个心
  • rust的数据结构还没学到,所以一次循环就懒了
  • 想出来了一半的思路,优化没想到队头的弹出,继续加油~

欢迎指正与讨论!