子序列和值最大,区间最短相关问题 (单调队列,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\) 是有范围的:
其次, \(s_{i}\) 作为一个常量,可以提到外面去:
从前向后维护一个长度不超过 \(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;
}
};