题目 - 点击直达
1658. 将 x 减到 0 的最小操作数 | 中等难度
1. 题目详情
给你一个整数数组 nums 和一个整数 x 。每一次操作时,你应当移除数组 nums 最左边或最右边的元素,然后从 x 中减去该元素的值。请注意,需要 修改 数组以供接下来的操作使用。
如果可以将 x 恰好 减到 0 ,返回 最小操作数 ;否则,返回 -1 。
示例 1:
输入:nums = [1,1,4,2,3], x = 5
输出:2
解释:最佳解决方案是移除后两个元素,将 x 减到 0 。
示例 2:
输入:nums = [5,6,7,8,9], x = 4
输出:-1
示例 3:
输入:nums = [3,2,20,1,1,3], x = 10
输出:5
解释:最佳解决方案是移除后三个元素和前两个元素(总共 5 次操作),将 x 减到 0 。
提示:
1 <= nums.length <= 105
1 <= nums[i] <= 104
1 <= x <= 109
1. 原题链接
leetcode 1658. 将 x 减到 0 的最小操作数
2. 基础框架
● Cpp代码框架
class Solution {
public:
int minOperations(vector<int>& nums, int x) {
}
};
2. 解题思路
1. 题目分析
(
1
)
(1)
(1) 本题要求 一个数组nums
,其中的元素都大于等于1;每次从数组nums
最左边或最右边移除一个数,如果移除元素的和可以恰好等于给定的x
,求最小的操作数;如果怎样移除都不等于x
就返回-1。
这里的移除操作实际上指的不是真正的删除,而是为了说明已经移除的元素不能再被移除(被使用)了。
(
2
)
(2)
(2) 如果直接按照题目的思路进行模拟左右移除元素,我们会发现每一次移除具体是移除左边的元素还是移除右边的元素,这不好进行判断,代码也不好编写,思路也不清晰。
(
3
)
(3)
(3) 正难则反
:正向思考如何进行移除操作不好想,于是我们可以想想反向。
按照题目的要求,我们需要找的是左边和右边移除元素的个数最小且加和等于x
。给定的数组是不变的,所以所有元素的和是确定的,设为sumNums
,那么问题就可以转化为求中间区间元素的个数(长度)的最大值且加和sum == sumNums - x
(即sum + x == sumNums)。
而sumNumber - x
是确定的值,整体设为target
,那么上述转化又可以表示为:求nums
最大连续子数组且加和sum == target
。怎么样,熟悉不熟悉?滑动窗口来了。
2. 算法原理
(
1
)
(1)
(1) 转化为求nums
最大连续子数组且加和等于target
的问题后,依然先思考如何暴力枚举解题,为滑动窗口出现铺路:
以left
为起始位置,right
依次遍历,判断[left,right]
范围内的子数组加和sum
:
如果sum < target
,即sum < sumNums - x
,说明可以继续加入新元素,right++
;
如果sum > target
,即sum > sumNums - x
,说明当前right位置元素加入后,加和sum
已经不可能满足关系sum == target
了,而数组nums
中的元素都是nums[i] >= 1
,right
及其之后的所有元素都会导致sum
大于target
,所以right
没有后移的必要了,此时left
可以右移1位,right回退到left位置继续遍历,重新计算sum
。
如果sum == target
,即sum == sumNums - x
,正好满足要求,更新对应的len
(表示最大连续子数组的长度),之后right
后面的所有元素也就不用再遍历了,因为数组nums
中的元素都是nums[i] >= 1
,所以再新加入元素就一定会使sum
大于target
,没有必要,之后操作同sum > target
的末尾处理。
(
2
)
(2)
(2) 在分析暴力解法的时候,每次以left
为起始位置遍历子数组时,right
都会回退到left
位置,重新开始遍历和计算加和sum
。能不能right
不回退,而只是left
右移呢?答案当然是:能!
为什么呢?
假设以left
为起始位置开始遍历数组nums
,right
在某个位置满足了sum >= target
,之后left
不右移,right
也先不着急回退。我们定格此处分析:
因为right
位置的元素加入之后才出现sum >= target
的情况,所以[left, right - 1]
范围内的加和sum1
一定是小于target
的,那么[left + 1, right - 1]
范围内的子数组加和sum2
又小于sum1
,所以sum2
也一定小于target
。
那么,对于right
来说,有必要回退到left + 1
位置重新遍历吗?
很明显没有必要:因为即使right
重新遍历,那么从[left + 1, right - 1]
区间一定是总是满足加和小于target
的,那么right
一定又会再次来到right
回退之前的位置([left + 1, right]
子数组加和可能恰好等于target,也可能大于target,也可能小于target),无论是那种情况,right
都会至少来到上一次回退时候的位置。所以right
可以不回退,而是继续在本位置(加和可能等于或大于target)或继续向后移动(加和小于target)。
(
3
)
(3)
(3) 进行滑动窗口处理:
- 计算整个数组
nums
的加和sumNums
;定义target = sumNums - x
; - 特殊判断
target
是否小于0:如果小于0,说明数组加和小于x
,一定不存在满足题意的条件,则直接返回-1; - 初始化:定义记录最大连续子数组长度
len = -1
;定义滑动窗口边界left = 0, right = 0
;定义连续子数组加和sum = 0
; - 进窗口:新元素进入,
sum += nums[right]
; - 判断:
sum > target
时,(使sum趋近target,需要出元素) - 出窗口:
left
左移从滑动窗口划出,sum -= nums[left], left++
,直到sum <= target
为止; - 更新结果:只有在
sum == target
时,才更新len
,保存最大的连续子数组的长度; right
右移1位,为下一个元素进窗口做准备;- 因为题目要的结果是在满足
加和恰好等于x
的左右两边元素的最小长度(个数),我们得到的是连续子数组的最大长度,所以返回nums.size() - len
或-1;
( 4 ) (4) (4)
3. 时间复杂度
暴力枚举 O ( n 2 ) O(n^2) O(n2)
滑动窗口 O ( n ) O(n) O(n)
left
和right
同向移动,直到结束,遍历一遍数组。
3. 代码实现
class Solution {
public:
int minOperations(vector<int>& nums, int x) {
int sumNums = 0;
for(auto e : nums) sumNums += e;
int target = sumNums - x;
if(target < 0) return - 1;
int l = 0, r = 0, sum = 0, n = nums.size(), len = -1;
while(r < n){
sum += nums[r];
while(sum > target) sum -= nums[l++];
if(sum == target) len = max(len, r - l + 1);
r++;
}
return len == -1 ? len : n - len;
}
};
4. 知识与收获
(
1
)
(1)
(1) 解决本题关键的一点就是对问题进行转化,当一道题正着按照题目的要求十分难做、没有思路时,往往考虑问题的反面时能找到好想的思路,所谓正难则反就是这样。
(
2
)
(2)
(2) 滑动窗口解题,对于具体的题目需要注意到不同题的细节处理,本题中所有元素都是大于等于1的,不会出现负数。sumNums - x
可能出现负数情况,x也可能是0,此时最小操作数就是0,即移除0个元素。
T h e The The E n d End End