Hard
题目描述
给你一个由正整数组成的下标从 0 开始的数组 nums。
如果移除 nums 的某个子数组后,nums 变成严格递增的数组,则这个子数组称为可移除子数组。例如,[3, 4] 是数组 [5, 3, 4, 6, 7] 的一个可移除子数组,因为移除这个子数组后,数组 [5, 3, 4, 6, 7] 变成 [5, 6, 7],这是一个严格递增的数组。
返回 nums 中可移除子数组的总数目。
注意,空数组被认为是严格递增的。
子数组是数组中一个连续的非空元素序列。
示例 1:
输入:nums = [1,2,3,4]
输出:10
解释:10 个可移除子数组分别为:[1], [2], [3], [4], [1,2], [2,3], [3,4], [1,2,3], [2,3,4], 和 [1,2,3,4],因为移除任何一个子数组后,nums 都会变成严格递增的。注意不能选择空子数组。
示例 2:
输入:nums = [6,5,7,8]
输出:7
解释:7 个可移除子数组分别为:[5], [6], [5,7], [6,5], [5,7,8], [6,5,7] 和 [6,5,7,8]。
可以证明 nums 中只有 7 个可移除子数组。
示例 3:
输入:nums = [8,7,6,6]
输出:3
解释:3 个可移除子数组分别为:[8,7,6], [7,6,6], 和 [8,7,6,6]。注意 [8,7] 不是可移除子数组,因为移除 [8,7] 后 nums 变成 [6,6],这是按升序排序的但不是严格递增的。
提示:
1 <= nums.length <= 10^51 <= nums[i] <= 10^9
解题思路
这是一个双指针优化问题。核心思想是找出数组的递增前缀和递增后缀,然后计算它们的有效组合数。
分析思路:
- 找到最大的递增前缀:从左到右找到最长的严格递增序列,记录下标为
left - 找到最小的递增后缀:从右到左找到最长的严格递增序列,记录下标为
right - 对于每个保留的前缀
nums[0...i],需要找到能与之拼接的后缀nums[j...n-1],使得nums[i] < nums[j]
关键观察:
- 如果整个数组都是递增的,那么可以移除任何子数组,总数为
n*(n+1)/2 - 否则,我们需要枚举保留的前缀长度,对于每个前缀,使用双指针找到合适的后缀起始位置
- 当前缀长度增加时,后缀的起始位置单调不减,这是双指针优化的关键
算法步骤:
- 计算递增前缀的最大长度
- 计算递增后缀的最小起始位置
- 特殊处理:如果整个数组递增,直接返回
n*(n+1)/2 - 使用双指针枚举前缀和后缀的组合,统计可移除子数组数量
代码实现
class Solution {
public:
long long incremovableSubarrayCount(vector<int>& nums) {
int n = nums.size();
// Find the longest increasing prefix
int left = 0;
while (left < n - 1 && nums[left] < nums[left + 1]) {
left++;
}
// If the whole array is increasing
if (left == n - 1) {
return (long long)n * (n + 1) / 2;
}
long long result = left + 2;
// Extend an increasing suffix from right to left. For each suffix,
// keep only prefixes whose final value is smaller than its first value.
for (int right = n - 1; right > 0; right--) {
while (left >= 0 && nums[left] >= nums[right]) {
left--;
}
result += left + 2;
if (nums[right - 1] >= nums[right]) {
break;
}
}
return result;
}
};
class Solution:
def incremovableSubarrayCount(self, nums: List[int]) -> int:
n = len(nums)
# Find the longest increasing prefix
left = 0
while left < n - 1 and nums[left] < nums[left + 1]:
left += 1
# If the whole array is increasing
if left == n - 1:
return n * (n + 1) // 2
result = left + 2
# 从右向左扩展递增后缀,并收缩无法与后缀拼接的前缀
for right in range(n - 1, 0, -1):
while left >= 0 and nums[left] >= nums[right]:
left -= 1
result += left + 2
if nums[right - 1] >= nums[right]:
break
return result
public class Solution {
public long IncremovableSubarrayCount(int[] nums) {
int n = nums.Length;
// Find the longest increasing prefix
int left = 0;
while (left < n - 1 && nums[left] < nums[left + 1]) {
left++;
}
// If the whole array is increasing
if (left == n - 1) {
return (long)n * (n + 1) / 2;
}
long result = left + 2;
// Extend an increasing suffix and shrink incompatible prefixes.
for (int right = n - 1; right > 0; right--) {
while (left >= 0 && nums[left] >= nums[right]) {
left--;
}
result += left + 2;
if (nums[right - 1] >= nums[right]) {
break;
}
}
return result;
}
}
/**
* @param {number[]} nums
* @return {number}
*/
var incremovableSubarrayCount = function(nums) {
const n = nums.length;
let left = 0;
while (left < n - 1 && nums[left] < nums[left + 1]) {
left++;
}
if (left === n - 1) {
return (n * (n + 1)) / 2;
}
let result = left + 2;
for (let right = n - 1; right > 0; right--) {
while (left >= 0 && nums[left] >= nums[right]) {
left--;
}
result += left + 2;
if (nums[right - 1] >= nums[right]) {
break;
}
}
return result;
};
复杂度分析
| 项目 | 复杂度 |
|---|---|
| 时间复杂度 | O(n) |
| 空间复杂度 | O(1) |
时间复杂度分析:
- 找递增前缀:O(n)
- 找递增后缀:O(n)
- 双指针遍历:O(n),因为每个指针最多移动n次
- 总时间复杂度:O(n)
空间复杂度分析:
- 只使用了常数个变量存储指针位置和计数结果
- 空间复杂度:O(1)