1. 程式人生 > 其它 >子序列和值最大,區間最短相關問題 (單調佇列,DP)

子序列和值最大,區間最短相關問題 (單調佇列,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] \]

時間複雜度

  • 狀態數為 \(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的元素。

時間複雜度

  • 每個元素最多進隊一次,出隊一次,故時間複雜度為 \(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;
    }
};