子序列和值最大,區間最短相關問題 (單調佇列,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]
表示,所以狀態轉移公式為:
時間複雜度
- 狀態數為 \(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\) 個數字為結尾的最大連續子序列的 總和 是多少。
狀態計算:
觀察這個轉移方程,首先這裡的 \(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)\)
- 構造字首和陣列 \(s(i)\),對於每個 \(i\),找到下標最大的 \(j\), \((j<i)\),使得 \(s(j) + K \le s(i)\),則以 \(i\) 結尾的最小的答案就是 \(i-j\)。
- 將所有 \(0, s(i), K, s(i) + K\) 進行離散化,離散化到 \([1, 2n + 2]\),然後使用權值樹狀陣列尋找最大的 \(j\)。
- 具體地,在對應 \(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;
}
};