Hard
题目描述
给你一个下标从 0 开始的整数数组 nums。
你可以执行任意次操作,每次操作包括选择数组的一个子数组,并将其替换为该子数组元素的和。例如,给定数组 [1,3,5,6],如果选择子数组 [3,5],数组将变为 [1,8,6]。
返回应用操作后可以得到的非递减数组的最大长度。
子数组是数组中连续的非空元素序列。
示例 1:
输入:nums = [5,2,2]
输出:1
解释:长度为 3 的数组不是非递减的。
我们有两种方法使数组长度变为 2。
首先,选择子数组 [2,2] 将数组转换为 [5,4]。
其次,选择子数组 [5,2] 将数组转换为 [7,2]。
这两种方法得到的数组都不是非递减的。
如果我们选择子数组 [5,2,2] 并将其替换为 [9],它就变成非递减的了。
所以答案是 1。
示例 2:
输入:nums = [1,2,3,4]
输出:4
解释:数组是非递减的。所以答案是 4。
示例 3:
输入:nums = [4,3,2,6]
输出:3
解释:将 [3,2] 替换为 [5] 将给定数组转换为 [4,5,6],这是非递减的。
因为给定数组不是非递减的,所以最大可能答案是 3。
约束条件:
1 <= nums.length <= 10^51 <= nums[i] <= 10^5
解题思路
解题思路
这是一道复杂的动态规划问题,核心思想是找到最优的合并策略来构造最长的非递减序列。
动态规划分析
定义 dp[i] 为处理前 i 个元素后能得到的最大非递减序列长度。同时需要记录每个状态对应的最后一个元素值。
关键观察:
- 对于位置
i,我们可以选择将nums[j:i]这段区间合并成一个元素,其值为这段区间的和 - 合并后的值必须 ≥ 前一段的最后元素值,才能保证非递减性
- 我们要在所有可行的
j中选择使dp[j] + 1最大的方案
优化策略
朴素的动态规划是 O(n²) 的,需要优化:
- 单调队列优化:维护一个单调队列,队列中存储
(dp值, 对应的最后元素值, 前缀和)的信息 - 前缀和:快速计算区间和
- 二分查找:在单调队列中快速找到满足条件的最优转移点
算法流程:
- 计算前缀和数组
- 维护单调队列,队列按 dp 值递增,最后元素值也递增
- 对每个位置
i,在队列中二分查找最大的满足条件的 dp 值 - 更新当前 dp 值,并将新状态加入队列,维护单调性
这种方法将复杂度降低到 O(n log n)。
代码实现
class Solution {
public:
int findMaximumLength(vector<int>& nums) {
int n = nums.size();
vector<long long> prefix(n + 1);
for (int i = 0; i < n; i++) {
prefix[i + 1] = prefix[i] + nums[i];
}
vector<int> dp(n + 1);
vector<long long> last(n + 1);
deque<pair<int, int>> dq; // (dp value, index)
dq.push_back({0, 0});
for (int i = 1; i <= n; i++) {
// Find the best j such that prefix[i] - prefix[j] >= last[j]
int left = 0, right = dq.size() - 1, best = -1;
while (left <= right) {
int mid = (left + right) / 2;
int j = dq[mid].second;
if (prefix[i] - prefix[j] >= last[j]) {
best = mid;
left = mid + 1;
} else {
right = mid - 1;
}
}
if (best != -1) {
int j = dq[best].second;
dp[i] = dp[j] + 1;
last[i] = prefix[i] - prefix[j];
} else {
dp[i] = 1;
last[i] = prefix[i];
}
// Maintain monotonic deque
while (!dq.empty() && (dq.back().first < dp[i] ||
(dq.back().first == dp[i] && last[dq.back().second] >= last[i]))) {
dq.pop_back();
}
dq.push_back({dp[i], i});
}
return dp[n];
}
};
class Solution:
def findMaximumLength(self, nums: List[int]) -> int:
from collections import deque
import bisect
n = len(nums)
prefix = [0] * (n + 1)
for i in range(n):
prefix[i + 1] = prefix[i] + nums[i]
dp = [0] * (n + 1)
last = [0] * (n + 1)
dq = deque([(0, 0)]) # (dp value, index)
for i in range(1, n + 1):
# Binary search for the best j
left, right = 0, len(dq) - 1
best = -1
while left <= right:
mid = (left + right) // 2
j = dq[mid][1]
if prefix[i] - prefix[j] >= last[j]:
best = mid
left = mid + 1
else:
right = mid - 1
if best != -1:
j = dq[best][1]
dp[i] = dp[j] + 1
last[i] = prefix[i] - prefix[j]
else:
dp[i] = 1
last[i] = prefix[i]
# Maintain monotonic deque
while dq and (dq[-1][0] < dp[i] or
(dq[-1][0] == dp[i] and last[dq[-1][1]] >= last[i])):
dq.pop()
dq.append((dp[i], i))
return dp[n]
public class Solution {
public int FindMaximumLength(int[] nums) {
int n = nums.Length;
long[] prefix = new long[n + 1];
for (int i = 0; i < n; i++) {
prefix[i + 1] = prefix[i] + nums[i];
}
int[] dp = new int[n + 1];
long[] last = new long[n + 1];
var dq = new List<(int dp, int index)>();
dq.Add((0, 0));
for (int i = 1; i <= n; i++) {
// Binary search for the best j
int left = 0, right = dq.Count - 1, best = -1;
while (left <= right) {
int mid = (left + right) / 2;
int j = dq[mid].index;
if (prefix[i] - prefix[j] >= last[j]) {
best = mid;
left = mid + 1;
} else {
right = mid - 1;
}
}
if (best != -1) {
int j = dq[best].index;
dp[i] = dp[j] + 1;
last[i] = prefix[i] - prefix[j];
} else {
dp[i] = 1;
last[i] = prefix[i];
}
// Maintain monotonic deque
while (dq.Count > 0 && (dq[dq.Count - 1].dp < dp[i] ||
(dq[dq.Count - 1].dp == dp[i] && last[dq[dq.Count - 1].index] >= last[i]))) {
dq.RemoveAt(dq.Count - 1);
}
dq.Add((dp[i], i));
}
return dp[n];
}
}
var findMaximumLength = function(nums) {
const n = nums.length;
const prefixSum = new Array(n + 1).fill(0);
for (let i = 0; i < n; i++) {
prefixSum[i + 1] = prefixSum[i] + nums[i];
}
const dp = new Array(n + 1).fill(0);
const last = new Array(n + 1).fill(0);
let j = 0;
for (let i = 1; i <= n; i++) {
while (j < i && prefixSum[i] - prefixSum[j] >= last[j]) {
if (dp[j] + 1 > dp[i] || (dp[j] + 1 === dp[i] && prefixSum[i] - prefixSum[j] < last[i])) {
dp[i] = dp[j] + 1;
last[i] = prefixSum[i] - prefixSum[j];
}
j++;
}
if (dp[i] === 0) {
dp[i] = dp[i - 1];
last[i] = last[i - 1];
}
}
return dp[n];
};
复杂度分析
| 复杂度类型 | 复杂度分析 |
|---|---|
| 时间复杂度 | O(n log n),其中 n 是数组长度。每个位置最多进出单调队列一次,每次二分查找的复杂度是 O(log n) |
| 空间复杂度 | O(n),需要存储前缀和数组、dp 数组、last 数组和单调队列 |