个人主页:C++忠实粉丝
欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 C++忠实粉丝 原创滑动窗口(4)_将x减到0的最小操作数
收录于专栏【经典算法练习】
本专栏旨在分享学习算法的一点学习笔记,欢迎大家在评论区交流讨论💌
目录
1. 题目链接:
2. 题目描述 :
3. 解法 :
解法一(暴力枚举) :
算法思路 :
具体步骤 :
代码展示 :
结果分析 :
对暴力算法的反思与优化
解法二(滑动窗口) :
算法思路 :
图解流程 :
代码展示 :
结果分析 :
1. 题目链接:
OJ链接:将x减到0的最小操作数
2. 题目描述 :
给你一个整数数组 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
3. 解法 :
解法一(暴力枚举) :
算法思路 :
我们可以发现这道题如果说按照题目的意思来写将会变得非常困难!x依次从开始和结尾取数相减,直到x等于0.那具体取了几个开头元素几个结尾元素我们不得而知,所以这里我们需要对题目进行转化: 找出最长的子数组的长度(len),所有元素的和正好等于 sum - x(这里的sum就是整个数组的和),我们最后的结果就是n-len
通过我们这样一转化,我们就可以使用暴力枚举,找出最长子数组的长度!
具体步骤 :
- 用两层循环遍历整个数组
- 用tmp记录两个指针区域见的总和
- 让右边的指针一直++,直到tmp >= target(sum - x),后面就不需要再遍历了,因为数组中的数>=1
- 如果此时tmp == target,记录下此时的最长子数组的长度
- i++,j=i.重复上述步骤
代码展示 :
class Solution {
public:
int minOperations(vector<int>& nums, int x) {
int sum = 0;
for(auto s : nums) sum += s;
int n = nums.size(), ret = -1, target = sum - x;
//x == sum 的情况
if(target == 0) return n;
//小优化
if(target < 1 && target != 0) return ret;
for(int i = 0; i < n; i++)
{
int tmp = 0;
for(int j = i; j < n; j++)
{
tmp += nums[j];
if(tmp == target) ret = max(ret, j - i + 1);
if(tmp > target) break;
}
}
if(ret == -1) return ret;
else return n - ret;
}
};
结果分析 :
老样子分析一下题目所给的范围:
题目vector数组中数的范围是在[0, 10^5]之内,而我们暴力算法的时间复杂度为O(N^2),总的数据级别超过10^10,这样系统会判定超时!!!
对暴力算法的反思与优化
10个暴力算法9个通过不了,但是暴力算法能给我们带来一些优化的思路:
就比如我们这道题,当我们暴力算法遍历到下面这种情况时:i++,j = i再重新继续遍历数组
我们就可以发现j其实不需要动,因为i++后,我们的tmp肯定减小,如果i++后,tmp还是大于target的话,我们只需让i一直++即可,因为就算让j重新等于i,那么同样会在j之前的位置大于target,我们这里就使用了滑动窗口让i,j之间的数永远小于target,这是一个动态维护的区间.
解法二(滑动窗口) :
算法思路 :
题目要求的是数组[左端 + 右端]两段连续的,和为x的最短数组,信息量稍微多一些,不易清理思路;我们就可以转化成求数组内一段连续的,和为sum(nums - x) 的最长数组(和暴力算法一样).此时,就是熟悉的[滑动窗口]问题了
图解流程 :
1.转化问题: 求target = sum(nums) - x.如果target < 0,问题无解
2.初始化左右指针left = 0, right = 0(滑动窗口区间表示为(left, right),左右区间是否
开闭很重要,必须设定与代码一致),记录当前滑动窗口内数组和的变量sum = 0,
记录当前满足条件数组的最大区间长度ret = -1
3. 当right小于等于数组长度时, 一直循环:
a. 如果sum < target, 右移右指针, 直至变量和大于等于target, 或右指针已经移到头
b. 如果sum > target, 右移左指针, 直至变量和小于等于target, 或左指针已经移到头
c. 如果经过前两步的左右移动使得sum == target, 维护满足条件数组的最大长度, 并让下一个元素进入窗口
代码展示 :
class Solution {
public:
int minOperations(vector<int>& nums, int x) {
int n = nums.size(), left = 0, right = 0, ret = -1;
int sum = 0, tmp = 0;
for(auto s : nums) sum += s;
int target = sum - x;
if(target < 0) return -1;
while(right < n)
{
tmp += nums[right];
if(tmp > target)
while(tmp > target) tmp -= nums[left++];
if(tmp == target) ret = max(ret, right - left + 1);
right++;
}
if(ret == -1) return ret;
else return n - ret;
}
};
结果分析 :
时间复杂度: O(N)
空间复杂度: O(1)
滑动窗口在这道题中是一种效率很高的解决方法.