Hard
题目描述
给你一个整数数组 nums 和一个整数 k,请你返回 非空 子序列元素和的最大值,子序列需要满足:对于连续的两个元素 nums[i] 和 nums[j],其中 i < j,这两个元素在原数组里的位置需要满足 j - i <= k。
数组的子序列定义为:通过删除数组中的一些元素(也可以不删除),剩余元素保持原来的顺序得到的数组。
示例 1:
输入:nums = [10,2,-10,5,20], k = 2
输出:37
解释:子序列为 [10, 2, 5, 20]。
示例 2:
输入:nums = [-1,-2,-3], k = 1
输出:-1
解释:子序列必须是非空的,所以我们选择最大的数字。
示例 3:
输入:nums = [10,-2,-10,-5,20], k = 2
输出:23
解释:子序列为 [10, -2, -5, 20]。
提示:
1 <= k <= nums.length <= 10^5-10^4 <= nums[i] <= 10^4
解题思路
这是一道经典的动态规划 + 单调队列优化题目。核心思想是使用动态规划求解,并利用单调队列优化时间复杂度。
思路分析:
动态规划状态定义:定义
dp[i]表示以位置i结尾的子序列的最大和。状态转移方程:对于位置
i,我们有两种选择:- 单独选择
nums[i]作为子序列 - 从前面
k个位置中选择一个最大的dp值,加上nums[i]
因此:
dp[i] = nums[i] + max(0, max(dp[i-k], dp[i-k+1], ..., dp[i-1]))- 单独选择
优化思路:朴素的动态规划需要 O(n×k) 的时间复杂度,我们可以使用单调双端队列来维护滑动窗口内的最大值,将时间复杂度优化到 O(n)。
单调队列维护:
- 队列存储下标,按照 dp 值单调递减排列
- 当窗口滑动时,移除超出范围的元素
- 队首始终是当前窗口内 dp 值最大的位置
这种方法既保证了正确性,又将时间复杂度优化到了最佳。
代码实现
class Solution {
public:
int constrainedSubsetSum(vector<int>& nums, int k) {
int n = nums.size();
vector<int> dp(n);
deque<int> dq;
int result = nums[0];
for (int i = 0; i < n; i++) {
// 移除超出窗口范围的元素
while (!dq.empty() && dq.front() < i - k) {
dq.pop_front();
}
// 计算当前位置的最大和
dp[i] = nums[i];
if (!dq.empty()) {
dp[i] = max(dp[i], nums[i] + dp[dq.front()]);
}
// 维护单调队列,移除队尾较小的元素
while (!dq.empty() && dp[dq.back()] <= dp[i]) {
dq.pop_back();
}
// 只有当dp[i] > 0时才加入队列(对后续位置有贡献)
if (dp[i] > 0) {
dq.push_back(i);
}
result = max(result, dp[i]);
}
return result;
}
};
class Solution:
def constrainedSubsetSum(self, nums: List[int], k: int) -> int:
from collections import deque
n = len(nums)
dp = [0] * n
dq = deque()
result = nums[0]
for i in range(n):
# 移除超出窗口范围的元素
while dq and dq[0] < i - k:
dq.popleft()
# 计算当前位置的最大和
dp[i] = nums[i]
if dq:
dp[i] = max(dp[i], nums[i] + dp[dq[0]])
# 维护单调队列
while dq and dp[dq[-1]] <= dp[i]:
dq.pop()
# 只有当dp[i] > 0时才加入队列
if dp[i] > 0:
dq.append(i)
result = max(result, dp[i])
return result
public class Solution {
public int ConstrainedSubsetSum(int[] nums, int k) {
int n = nums.Length;
int[] dp = new int[n];
var dq = new LinkedList<int>();
int result = nums[0];
for (int i = 0; i < n; i++) {
// 移除超出窗口范围的元素
while (dq.Count > 0 && dq.First.Value < i - k) {
dq.RemoveFirst();
}
// 计算当前位置的最大和
dp[i] = nums[i];
if (dq.Count > 0) {
dp[i] = Math.Max(dp[i], nums[i] + dp[dq.First.Value]);
}
// 维护单调队列
while (dq.Count > 0 && dp[dq.Last.Value] <= dp[i]) {
dq.RemoveLast();
}
// 只有当dp[i] > 0时才加入队列
if (dp[i] > 0) {
dq.AddLast(i);
}
result = Math.Max(result, dp[i]);
}
return result;
}
}
var constrainedSubsetSum = function(nums, k) {
const n = nums.length;
const dp = new Array(n);
const dq = [];
let result = nums[0];
for (let i = 0; i < n; i++) {
// 移除超出窗口范围的元素
while (dq.length > 0 && dq[0] < i - k) {
dq.shift();
}
// 计算当前位置的最大和
dp[i] = nums[i];
if (dq.length > 0) {
dp[i] = Math.max(dp[i], nums[i] + dp[dq[0]]);
}
// 维护单调队列
while (dq.length > 0 && dp[dq[dq.length - 1]] <= dp[i]) {
dq.pop();
}
// 只有当dp[i] > 0时才加入队列
if (dp[i] > 0) {
dq.push(i);
}
result = Math.max(result, dp[i]);
}
return result;
};
复杂度分析
| 项目 | 复杂度 |
|---|---|
| 时间复杂度 | O(n) |
| 空间复杂度 | O(n) |
说明:
- 时间复杂度:虽然有嵌套循环,但每个元素最多入队和出队一次,所以总时间复杂度是 O(n)
- 空间复杂度:需要 O(n) 的 dp 数组和 O(k) 的单调队列空间,总体为 O(n)