题目;
我们定义 arr
是 山形数组 当且仅当它满足:
arr.length >= 3
- 存在某个下标
i
(从 0 开始) 满足0 < i < arr.length - 1
且:arr[0] < arr[1] < ... < arr[i - 1] < arr[i]
arr[i] > arr[i + 1] > ... > arr[arr.length - 1]
给你整数数组 nums
,请你返回将 nums
变成 山形状数组 的 最少 删除次数。
示例 1:
输入:nums = [1,3,1] 输出:0 解释:数组本身就是山形数组,所以我们不需要删除任何元素。
示例 2:
输入:nums = [2,1,1,5,6,2,3,1] 输出:3 解释:一种方法是将下标为 0,1 和 5 的元素删除,剩余元素为 [1,5,6,3,1] ,是山形数组。
提示:
3 <= nums.length <= 1000
1 <= nums[i] <= 109
- 题目保证
nums
删除一些元素后一定能得到山形数组。
解题:
示例 1
输入:nums = [1, 3, 1]
输出:0
解释:数组本身就是山形数组,所以我们不需要删除任何元素。
- 数组
[1, 3, 1]
本身就是一个符合山形数组定义的数组,先增加后减少,所以不需要删除任何元素。
示例 2
输入:nums = [2, 1, 1, 5, 6, 2, 3, 1]
输出:3
解释:一种方法是将下标为 0,1 和 5 的元素删除,剩余元素为 [1, 5, 6, 3, 1],是山形数组。
- 原数组
[2, 1, 1, 5, 6, 2, 3, 1]
,可以通过删除下标为0
,1
和5
位置的元素变成山形数组[1, 5, 6, 3, 1]
。 - 删除了元素
2
,1
,3
后,使得剩余数组满足山形数组的定义。
提示详解
-
提示 1:
3 <= nums.length <= 1000
:- 数组长度
nums
的范围为3
到1000
,说明需要一个相对高效的方法来处理可能较大的数组。
- 数组长度
-
提示 2:
1 <= nums[i] <= 10^9
:- 数组中的元素范围在
1
到1,000,000,000
之间,所以算法必须适应处理大范围整数值。
- 数组中的元素范围在
-
提示 3:题目保证
nums
删除一些元素后一定能得到山形数组。- 不需要考虑无法得到山形数组的情况,这简化了问题。
-
解决这个问题的关键在于理解如何利用动态规划(DP)来找到任意一个元素作为山顶时的最长山形子序列。这一过程可以分解为几个步骤:
-
定义状态:
- 我们维护两个DP数组来记录状态,即
lis[i]
和lds[i]
。lis[i]
代表以nums[i]
结尾的最长上升子序列的长度(Longest Increasing Subsequence, LIS)。lds[i]
代表从nums[i]
开始的最长下降子序列的长度(Longest Decreasing Subsequence, LDS)。
- 我们维护两个DP数组来记录状态,即
-
状态转移:
- 对于
lis[i]
,我们通过比较nums[i]
与其之前的元素nums[j]
(j < i
)来更新lis[i]
的值。 - 对于
lds[i]
,我们通过比较nums[i]
与其之后的元素nums[j]
(j > i
)来更新lds[i]
的值。
- 对于
-
合并上升和下降序列:
- 一旦我们有了每个元素为山顶时的上升和下降序列长度,我们就可以找到以该元素为山顶时的最长山形序列的长度,即
lis[i] + lds[i] - 1
(因为山顶元素被计算了两次,需减去1)。
- 一旦我们有了每个元素为山顶时的上升和下降序列长度,我们就可以找到以该元素为山顶时的最长山形序列的长度,即
-
找到最长的山形子序列:
- 遍历所有元素,利用上述方法计算以每个元素为山顶的最长山形序列长度,取最大值
maxLength
。
- 遍历所有元素,利用上述方法计算以每个元素为山顶的最长山形序列长度,取最大值
-
计算最小删除次数:
- 最后,用数组总长减去
maxLength
,即得到了得到山形数组的最少删除次数。
- 最后,用数组总长减去
计算LIS:
for (int i = 0; i < n; i++) {
lis[i] = 1;
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) {
lis[i] = Math.max(lis[i], lis[j] + 1);
}
}
}
计算LDS:
for (int i = n - 1; i >= 0; i--) {
lds[i] = 1;
for (int j = n - 1; j > i; j--) {
if (nums[i] > nums[j]) {
lds[i] = Math.max(lds[i], lds[j] + 1);
}
}
}
找出最长的山形子序列并计算需要删除的元素数量:
int maxLength = 0;
for (int i = 0; i < n; i++) {
if (lis[i] > 1 && lds[i] > 1) {
maxLength = Math.max(maxLength, lis[i] + lds[i] - 1);
}
}
return n - maxLength;
代码:
public class Solution {
public int minimumMountainRemovals(int[] nums) {
int n = nums.length;
int[] lis = new int[n];
int[] lds = new int[n];
// 计算 LIS
for (int i = 0; i < n; i++) {
lis[i] = 1;
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) {
lis[i] = Math.max(lis[i], lis[j] + 1);
}
}
}
// 计算 LDS
for (int i = n - 1; i >= 0; i--) {
lds[i] = 1;
for (int j = n - 1; j > i; j--) {
if (nums[i] > nums[j]) {
lds[i] = Math.max(lds[i], lds[j] + 1);
}
}
}
// 找出最长的山形子序列
int maxLength = 0;
for (int i = 0; i < n; i++) {
if (lis[i] > 1 && lds[i] > 1) {
maxLength = Math.max(maxLength, lis[i] + lds[i] - 1);
}
}
// 返回需要删除的元素数量
return n - maxLength;
}
}
知识点解析:
-
动态规划(Dynamic Programming, DP):利用过去计算的结果来帮助解决后续的子问题,从而避免重复计算。这是解题的核心思想,通过分别计算最长上升子序列(LIS)和最长下降子序列(LDS)来找到最长的山形子序列。
-
数组操作:利用数组存储中间计算结果(如LIS和LDS的长度),以及最终的结果计算。
-
双层循环:用于计算LIS和LDS,内层循环遍历小于当前索引的所有元素,寻找合适的元素更新当前的LIS或LDS值。
-
条件判断:在计算LIS和LDS时,需要通过条件判断来确保只有在当前元素大于遍历到的元素时才进行计算。
-
数学运算:在各个步骤中用到了基本的数学运算,如最大值
Math.max()
的计算。
知识点 | 描述 | 应用 |
---|---|---|
动态规划 | 通过解决子问题的方式来解决主问题,避免重复计算。 | 分别计算最长上升子序列(LIS)和最长下降子序列(LDS)。 |
数组操作 | 使用数组来存储数据和结果。 | 存储LIS和LDS的长度及用于遍历查找的过程中的中间结果。 |
双层循环 | 使用嵌套循环遍历数组中的元素。 | 在计算LIS和LDS时,内层循环遍历并更新当前索引的LIS或LDS值。 |
条件判断 | 根据特定条件来执行不同的逻辑。 | 确保在适当的条件下(如要求严格递增/递减)更新LIS和LDS值。 |
数学运算 | 使用基本的数学运算和函数。 | 使用Math.max() 来找到最大的LIS、LDS值,以及计 |