Medium
题目描述
给你一个整数数组 nums。数组中的每个元素都是 1、2 或 3。在每次操作中,你可以从 nums 中删除一个元素。返回使 nums 非递减所需的最少操作次数。
示例 1:
输入:nums = [2,1,3,2,1]
输出:3
解释:
一个最优解是删除 nums[0]、nums[2] 和 nums[3]。
示例 2:
输入:nums = [1,3,2,1,3,3]
输出:2
解释:
一个最优解是删除 nums[1] 和 nums[2]。
示例 3:
输入:nums = [2,2,2,2,3,3]
输出:0
解释:
nums 已经是非递减的。
约束条件:
1 <= nums.length <= 1001 <= nums[i] <= 3
**进阶:**你能想出一个时间复杂度为 O(n) 的算法吗?
解题思路
这个问题要求我们删除最少的元素,使得数组变成非递减的。由于数组中只有 1、2、3 三个值,我们可以将问题转化为:保留最多的元素,使得剩余元素构成非递减序列。
方法一:枚举分割点(推荐)
核心思路是将数组分成三个连续区间:
- 区间1:[0, i) - 全部保留值为1的元素
- 区间2:[i, j) - 全部保留值为2的元素
- 区间3:[j, n) - 全部保留值为3的元素
对于每种可能的分割方案 (i, j),我们计算能保留的元素数量,然后用总长度减去最大保留数量即为最少删除次数。
具体做法:
- 枚举所有可能的分割点组合 (i, j),其中 0 ≤ i ≤ j ≤ n
- 对每种分割方案,统计各区间内目标值的数量
- 计算该方案下能保留的元素总数
- 取所有方案中保留元素最多的情况
方法二:动态规划
也可以用DP求解最长非递减子序列,由于只有3个不同值,状态空间很小。
时间复杂度:O(n²) 枚举分割点,O(n) 动态规划
空间复杂度:O(1)
代码实现
class Solution {
public:
int minimumOperations(vector<int>& nums) {
int n = nums.size();
int maxKeep = 0;
// 枚举所有可能的分割点 (i, j)
for (int i = 0; i <= n; i++) {
for (int j = i; j <= n; j++) {
int keep = 0;
// 区间[0, i)保留所有1
for (int k = 0; k < i; k++) {
if (nums[k] == 1) keep++;
}
// 区间[i, j)保留所有2
for (int k = i; k < j; k++) {
if (nums[k] == 2) keep++;
}
// 区间[j, n)保留所有3
for (int k = j; k < n; k++) {
if (nums[k] == 3) keep++;
}
maxKeep = max(maxKeep, keep);
}
}
return n - maxKeep;
}
};
class Solution:
def minimumOperations(self, nums: List[int]) -> int:
n = len(nums)
max_keep = 0
# 枚举所有可能的分割点 (i, j)
for i in range(n + 1):
for j in range(i, n + 1):
keep = 0
# 区间[0, i)保留所有1
for k in range(i):
if nums[k] == 1:
keep += 1
# 区间[i, j)保留所有2
for k in range(i, j):
if nums[k] == 2:
keep += 1
# 区间[j, n)保留所有3
for k in range(j, n):
if nums[k] == 3:
keep += 1
max_keep = max(max_keep, keep)
return n - max_keep
public class Solution {
public int MinimumOperations(IList<int> nums) {
int n = nums.Count;
int maxKeep = 0;
// 枚举所有可能的分割点 (i, j)
for (int i = 0; i <= n; i++) {
for (int j = i; j <= n; j++) {
int keep = 0;
// 区间[0, i)保留所有1
for (int k = 0; k < i; k++) {
if (nums[k] == 1) keep++;
}
// 区间[i, j)保留所有2
for (int k = i; k < j; k++) {
if (nums[k] == 2) keep++;
}
// 区间[j, n)保留所有3
for (int k = j; k < n; k++) {
if (nums[k] == 3) keep++;
}
maxKeep = Math.Max(maxKeep, keep);
}
}
return n - maxKeep;
}
}
/**
* @param {number[]} nums
* @return {number}
*/
var minimumOperations = function(nums) {
const n = nums.length;
const dp = Array(n).fill().map(() => Array(4).fill(0));
// dp[i][j] = max length of non-decreasing subsequence ending at index i with value j
for (let i = 0; i < n; i++) {
for (let j = 1; j <= 3; j++) {
if (nums[i] == j) {
dp[i][j] = 1;
if (i > 0) {
for (let k = 1; k <= j; k++) {
dp[i][j] = Math.max(dp[i][j], dp[i-1][k] + 1);
}
}
} else if (i > 0) {
dp[i][j] = dp[i-1][j];
}
}
}
return n - Math.max(dp[n-1][1], dp[n-1][2], dp[n-1][3]);
};
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 枚举分割点 | O(n³) | O(1) |
| 动态规划 | O(n) | O(1) |
说明:
- 枚举分割点方法:外层两个循环 O(n²),内层统计 O(n),总体 O(n³)
- 由于数据规模较小(n ≤ 100),O(n³) 的解法完全可行
- 动态规划可以优化到 O(n),但实现稍复杂,在此数据规模下枚举法更直观