子序列和值最大,区间最短相关问题 (单调队列,DP)

Leetcode53. 最大子数组和 求最大值

题目描述

给定一个整数数组 nums,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

样例

输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6。


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


输入:nums = [0]
输出:0


输入:nums = [-1]
输出:-1


输入:nums = [-100000]
输出:-100000

限制

  • 1 <= nums.length <= 3 * 10^4
  • -10^5 <= nums[i] <= 10^5

进阶

  • 如果你已经实现复杂度为 \(O(n)\) 的解法,尝试使用更为精妙的 分治法 求解。

算法

(动态规划) \(O(n)\)

状态表示:设 \(f(i)\) 表示以第 \(i\) 个数字为结尾的最大连续子序列的 总和 是多少。

状态划分:

集合可以划分为两种:

  • 选以i-1结尾的连续子数组和nums[i]
  • nums[i]

具体的为nums[i, i]、nums[i-1, i]、nums[k, i]、nums[1, i]、nums[0, i]集合,如图,对于图中,蓝色部分,发现可以用f[i - 1]表示,所以状态转移公式为:

\[f[i] = max(f[i - 1] + nums[i], nums[i])\\ =max(f[i - 1], 0) + nums[i] \]

image-20220116152408383

时间复杂度

  • 状态数为 \(O(n)\),转移时间为 \(O(1)\),故总时间复杂度为 \(O(n)\)

空间复杂度

  • 需要额外 \(O(n)\) 的空间存储状态。
  • 可以通过一个变量来替代数组将空间复杂度优化到常数。

C++ 代码

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int res = INT_MIN;
        for (int i = 0, last = 0; i < nums.size(); i ++ ) {
            last = nums[i] + max(last, 0);
            res = max(res, last);
        }
        return res;
    }
};

分治 时间O(n),空间O(logn)(线段树思想)

class Solution {
public:

    struct status {
        int sum, s, ls, rs; // 区间总和, 最大子段和, 最大前缀和, 最大后缀和
    };

    status build(vector<int>& nums, int l, int r) {
        if (l == r) return {nums[l], nums[l], nums[l], nums[l]};

        int mid = l + r >> 1;
        auto L = build(nums, l, mid), R = build(nums, mid + 1, r);
        status LR;
        LR.sum = L.sum + R.sum;
        LR.s = max(max(L.s, R.s), L.rs + R.ls);
        LR.ls = max(L.ls, L.sum + R.ls);
        LR.rs = max(R.rs, R.sum + L.rs);
        return LR;
    }

    int maxSubArray(vector<int>& nums) {
        int n = nums.size();
        auto res = build(nums, 0, n - 1);
        return res.s;
    }
}

AcWing 135. 最大子序和

题目描述
给定一个长度为 n 的序列 a,找出其中 元素总和最大 且 长度 不超过 m 的 连续子区间。

输入样例:

6 4
1 -3 5 1 -2 3

输出样例:

7

算法

**(动态规划) **

s[i]表示原数组的前缀和,则区间[l, r]和为s[r] - s[l-1]

状态表示:设 \(f(i)\) 表示以第 \(i\) 个数字为结尾的最大连续子序列的 总和 是多少。
状态计算:

\[f_{i}=\max \left\{s_{i}-s_{j}\right\} \quad(1 \leq i-j \leq m) \]

观察这个转移方程,首先这里的 \(j\) 是有范围的:

\[i-m \leq j \leq i-1 \]

其次, \(s_{i}\) 作为一个常量,可以提到外面去:

\[f_{i}=s_{i}-\min \left\{s_{j}\right\} \quad(1 \leq i-j \leq m) \]

从前向后维护一个长度不超过 \(m\) 的区间的最小值,单调队列(递增)即可以解决这个问题。

#include<bits/stdc++.h>
using namespace std;
const int N = 300010;
int n, m;
int a[N], s[N];

int main()
{
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; i++){
        scanf("%d", &a[i]);
        s[i] = s[i - 1] + a[i];
    }
    deque<int> q;
    q.push_back(0);
    int res = -1e9;
    for(int i = 1; i <= n; i++){
        while(!q.empty() && i - q.front() > m ) q.pop_front();
        res = max(res, s[i] - s[q.front()]);
        while(!q.empty() && s[q.back()] >= s[i]) q.pop_back();
        q.push_back(i);
    }
    cout<<res<<endl;
    return 0;
}

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

题目描述

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

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

样例

输入:A = [1], K = 1
输出:1


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


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

注意

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

算法1

(单调队列) \(O(n)\)

s[i]表示原数组的前缀和,则区间[l, r]和为s[r] - s[l-1]

题目可以转化为:满足s[i] - s[j] >= k, 最小``j - i + 1`的区间。

遍历s[1 ~ i],对于s[i]如何找到最优的s[j]呢?

维护一个单调递增的队列q,如图蓝色+红色线段所示,当到达s[i]的时候,主要有两个操作:

  • 蓝色部分:从 \(j = q.front()\) 开始,在满足 \(s(j) + K \le s(i)\) 的情况下一直向后移动 \(j\),且将元素弹出队头,直到条件不满足,此时 \(s(i)\) 所能更新的答案就是 \(i - j\)。为什么队头遍历过元素可以丢弃吗?因为i时从前到后遍历,越往后肯定越大,与j的距离肯定越远,答案肯定没有当前的i优秀。
  • 红色部分:可以直接舍弃,假设在i的后面t位置满足\(s[t] - s[j] >= k\),但时\(t - j >= i - j\),所以s[i]保存到单调队列肯定优于集合满足s[j] >= s[i],0<=j<i的元素。

image-20220116172155237

时间复杂度

  • 每个元素最多进队一次,出队一次,故时间复杂度为 \(O(n)\)

空间复杂度

  • 需要 \(O(n)\) 的额外空间存储前缀和数组和单调队列。

C++ 代码

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

(树状数组) \(O(nlog n)\)

  1. 构造前缀和数组 \(s(i)\),对于每个 \(i\),找到下标最大的 \(j\), \((j<i)\),使得 \(s(j) + K \le s(i)\),则以 \(i\) 结尾的最小的答案就是 \(i-j\)
  2. 将所有 \(0, s(i), K, s(i) + K\) 进行离散化,离散化到 \([1, 2n + 2]\),然后使用权值树状数组寻找最大的 \(j\)
  3. 具体地,在对应 \(s\) 值的树状数组位置上更新最大值。查询时从树状数组中寻找一个前缀最大值,然后再更新树状数组即可。

时间复杂度

  • 每次更新和查询的时间复杂度为 \(O(log n)\),故总时间复杂度为 \(O(nlog n)\)

C++ 代码

class Solution {
public:

    void update(vector<int> &f, int x, int y) {
        for (; x < f.size(); x += x & -x)
            f[x] = max(f[x], y);
    }

    int query(vector<int> &f, int x) {
        int t = -1;
        for (; x; x -= x & -x)
            t = max(f[x], t);
        return t;
    }

    int shortestSubarray(vector<int>& A, int K) {
        int n = A.size();
        vector<int> s(2 * n + 2, 0), d(2 * n + 2, 0);
        vector<int> f(2 * n + 3, -1);

        for (int i = 1; i <= n; i++) {
            s[i] = s[i - 1] + A[i - 1];
            d[i] = s[i];
        }
        for (int i = n + 1; i <= 2 * n + 1; i++) {
            s[i] = s[i - n - 1] + K;
            d[i] = s[i];
        }

        sort(d.begin(), d.end());

        for (int i = 0; i <= 2 * n + 1; i++)
            s[i] = lower_bound(d.begin(), d.end(), s[i]) - d.begin() + 1;

        update(f, s[n + 1], 0);

        int ans = n + 1;

        for (int i = 1; i <= n; i++) {
            int t = query(f, s[i]);
            if (t != -1)
                ans = min(ans, i - t);
            update(f, s[i + n + 1], i);
        }

        if (ans == n + 1)
            ans = -1;

        return ans;
    }
};
posted @ 2022-01-16 20:13  pxlsdz  阅读(1283)  评论(0编辑  收藏  举报