Leetcode 862:和至少为 K 的最短子数组

1,165 阅读2分钟

Offer 驾到,掘友接招!我正在参与2022春招打卡活动,点击查看活动详情


ps:前几天虾皮面试的时候问了这个题。

题目描述

Leetcode 862: 和至少为 K 的最短子数组

难度:Hard

标签:单调队列

一个长度为 nn 数组,有正有负,求一个最短的子数组满足 numsi>=k\sum nums_i >= k ,输出长度。

数据范围

1n105105numsi105\quad \quad 1 \le n \le 10^5 \\ \quad \quad -10^5 \le nums_i \le 10^5

测试用例

输入: nums = [1], k = 1
输出: 1

输入: nums = [1,2], k = 4
输出: -1

输入: nums = [2,-1,2], k = 3
输出: 3

思路分析

做法1-标准解法:单调队列

  首先我们知道子区间问题,要么动态规划,要么滑动窗口,要么前缀和,这个题目是相加,但动态规划和滑窗似乎不太好做。所以考虑利用前缀和的情况下如何去处理。先预处理完了前缀和,题目就变成了:当前值是 prefipref_i,需要在 0...i10...i-1 之间找到一个值 xx 满足 prefixkpref_i - x \ge k,也就是:xprefikx \le pref_i - k,仔细考察一下这个题的性质,会发现:

  1. 假设当前下标是 ii,存在一个前缀下标 xx 满足要求,就可以更新一下答案,并且在 xx 之前的所有值都可以不用再管了,因为后续更新答案的时候如果选他们,长度一定比现在 x...ix...i 大。 这也就是说,从头部出队,找到满足条件的值中最大的下标,前面小于该下标的值都可以不用管了。

image.png

  1. 考虑入队一个值时,如果现在这个值比队尾的小,那说明以后都不可能考虑那个元素了,因为它太大了而且下标还早于当前值,后面更新答案的时候就是选 ii 也不可能选它,所以那个值要出队了。(有一个后辈既比你强又比你年轻,你不就没用可以直接被踢了么)

image.png

到这里已经很明显了,单调队列完美满足这种需求。

AC 代码(单调队列)

using ll = long long;
class Solution {
    constexpr static int INF = 0x3f3f3f3f;
public:
    int shortestSubarray(vector<int>& nums, int k) {
        vector<ll> pref{0};
        for (int i : nums) {
            pref.push_back(pref.back() + i);
        }
        int n = size(pref);
        int ans = INF;
        deque<int> q;
        for (int i = 0; i < n; i++) {
            while (!q.empty() and pref[i] - pref[q.front()] >= k) {
                ans = min(ans, i - q.front());
                q.pop_front();
            }
            while (!q.empty() and pref[i] < pref[q.back()]) {
                q.pop_back();
            }
            q.push_back(i);
        }
        return ans == INF ? -1 : ans;
    }
};

做法2-离散化+树状数组 维护前缀区间内满足条件的最大值

  在不知道前面的性质,分析不出来用的是单调队列的情况下,我们拿到前缀和数组 prefpref 后,知道对应 ii,需要找的是 0...i10...i-1 区间内一个最大的下标 jj 满足 prefiprefjkpref_i-pref_j \ge k,也就是求一个 max{j}max\{j\},满足: prejpreikpre_j \le pre_i-k

  这个操作可以通过树状数组做,但是因为 prefipref_iprefikpref_i - k 的值不是连续的,所以需要进行离散化,把所有的 prefipref_iprefikpref_i - k 离散到坐标 1...2n1...2*n 内。

  离散化完成后,用树状数组保存前缀区间内,满足小于等于 x 的值中所有坐标中的最大值,也就是说:用树状数组的 tree[i]tree[i] 代表变成 小于等于 i 的数中最大的那个坐标。这样,对于每个 prefipref_i,需要查询的就是 tree.query(prefik)tree.query(pref_i - k),这个值代表了满足条件的最大下标,那么就可以更新 ans=min(ans,itree.query(pref[i]k)ans = min(ans, i - tree.query(pref[i]-k) 了.

AC 代码(树状数组实现)O(nlogn)

using ll = long long;
constexpr static int INF = 0x3f3f3f3f;
struct BIT {
    int n;
    vector<int> tree;

    BIT(int n) { 
        this->n = n; 
        tree = vector<int>(n, -1); 
    }

    int lowbit(int x) {
        return x & (-x);
    }

    void insert(int id, int x) {
        for (int i = id; i < n; i += lowbit(i)) {
            tree[i] = max(x, tree[i]);
        }
    }

    int query(int id) {
        int ans = -1;
        for (int i = id; i > 0; i -= lowbit(i)) {
            ans = max(ans, tree[i]);
        }
        return ans;
    }
};

class Solution {
public:
    int shortestSubarray(vector<int>& nums, int k) {
        vector<ll> pre{0};
        for (int i = 0; i < size(nums); i++) {
            pre.push_back(pre.back() + nums[i]);
        }
        vector<ll> allNums;
        for (auto i : pre) {
            allNums.push_back(i);
            allNums.push_back(i - k);
        }
        sort(begin(allNums), end(allNums));
        unordered_map<int, int> ids;
        int cnt = 1;
        for (auto i : allNums) {
            if (!ids.count(i)) ids[i] = cnt++;
        }

        BIT tree(cnt + 1);
        int ans = INF;
        for (int i = 0; i < size(pre); i++) {
            int maxId = tree.query(ids[pre[i] - k]);
            if (maxId != -1) {
                ans = min(ans, i - maxId);
            }
            tree.insert(ids[pre[i]], i);
        }
        return ans == INF ? -1 : ans;
    }
};