LeetCode原题链接:1658. 将 x 减到 0 的最小操作数
下面是题目描述:
给你一个整数数组 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、解题思路
如果本题按照示例所给的过程进行枚举的话,情况其实会很复杂且不好控制,所以我们就需要将问题转换一下,这也是本文想记录和分享的重要解题方法,那就是 “正难则反”。
根据题目要求,我们需要通过从左右两端开始把其中左边或右边的数移除而判断是否能找到一个使x减为0的最小操作数(即要从两端开始找到几个数,使这个几个数的和加起来等于x,且所找的这几个数的数量要最小);
反过来,也就是说,需要在相对中间的位置判断是否能找到一段最长的连续的数(子数组)的和 等于 数组所有元素总和 - x。
若能找到,那么由于中间找到的连续子数组是最长的,此时用元素总个数减去中间的连续子数组的长度得到的就是所找的能使x减为0的最小操作数。
如示例3:
(1)“正着”操作:
最佳解决方案是移除后三个元素和前两个元素(总共 5 次操作),将 x 减到 0 。
(2)“反着”操作:
(PS:虽说是找到相对中间位置的连续最长子数组,但根据滑动窗口算法的执行过程本质还是从左端开始,是对暴力枚举的一种优化。)
综上所述,原问题就可以转换为:找一段长度最长的子数组且子数组的和等于一个值target,此类问题看上去就非常的 “滑动窗口”。算法的具体实现和笔者另一篇同知识点的题解很类似:【算法学习】-【滑动窗口】-【长度最小的子数组】,第一次使用滑动窗口算法的朋友们可以先看看那篇文章。
下面简单说明一下算法实现的基本步骤:
(1)进窗口
创建两个指针begin
和cur
,初始都指向第一个元素;变量sum
来判断当前总和是否等于目标值target
;若小于,则进窗口,即让sum加上当前元素:sum+=nums[begin]
;若大于等于,则进行下一步
(2)出窗口和更新数据
出窗口前,若总和sum
的值等于目标值 target
,cur - begin
为本次枚举所得的子数组的长度,将其与上一次枚举所得的结果进行对比,取较大者:len = len > cur - begin ? len : cur - begin
。需要注意的是,此时不会在本次循环(等于目标值时)出窗口,而是再让下一个元素进窗口而大于目标值target
后,才出窗口而准备进行下一次枚举。
2、具体代码
//找到中间最长连续子数组
int maxSubArrayLen(int target, vector<int>& nums)
{
//由于是间接求,需要注意target可能小于零
if(target < 0)
{
return 0;
}
int len = 0;
int sum = 0;
int begin = 0;
int cur = 0;
while(cur < nums.size())
{
//进窗口
sum += nums[cur++];
//出窗口和更新
if(sum == target)
{
len = len > cur - begin ? len : cur - begin;
}
while(sum > target)
{
sum -= nums[begin++];
}
}
return len;
}
//最小操作数
int minOperations(vector<int>& nums, int x)
{
int sum = 0;
for(auto e : nums)
{
sum += e;
}
if(sum - x == 0)
{
return nums.size();
}
int ret = maxSubArrayLen(sum - x, nums);
if(ret == 0)
{
return -1;
}
return nums.size() - ret;
看完觉得有觉得帮助的话不妨点赞收藏鼓励一下,有疑问或看不懂的地方或有可优化的部分还恳请朋友们留个评论,多多指点,谢谢朋友们!