将 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 <= nums.length <= 105
- 1 <= nums[i] <= 104
- 1 <= x <= 109
这道题直接能想到的解法是模拟,但是数据量太大了,模拟肯定会超时。那么有没有什么优化的方法?
- 一种是利用前缀和,因为是从头删除和从尾删除,而且是连续的数组,符合前缀和的特征。
- 第二种方法,倒过来思考,我们不选择删除,而是用滑动窗口找出中间最长的序列,这样就是删除最少的。
前缀和
从前删除和从后删除可以分为前缀和、后缀和。前缀和+后缀和=x。所以在已知x和后缀和的时候,只需要判断前缀和里面是否存在x-后缀和。
class Solution {
public int minOperations(int[] nums, int x) {
HashMap<Integer,Integer> lr = new HashMap();
int len = nums.length;
int k = 0;
int res = Integer.MAX_VALUE;
// 存储前缀和
for (int i = 0; i < len; i++) {
k += nums[i];
if (k == x) {
// 不需要后缀,前缀就能满足
res = i + 1;
}
lr.put(k,i);
}
int sum = k;
k = 0;
// 遍历后缀
for (int i = len -1; i >= 0; i--) {
k += nums[i];
int t = x - k;
if (t == 0) {
// 不需要前缀和,自己就满足
res = Math.min(res,len - i);
continue;
}
if (lr.containsKey(t)) {
// 含有另一半
int v = lr.get(t);
if (v < i) {
// 如果另一半的index在自己右边是不行的
// 测试样例[1,1]3
res = Math.min(res,lr.get(t) + 1+(len - i));
}
}
}
return res == Integer.MAX_VALUE ? -1 : res;
}
}
滑动窗口
首先找到一个大于等于sum-x的滑动窗口,然后让左窗口向后移动,如果依然满足sum-x,那么就说明这个窗口满足条件,判断一次;如果不满足sum-x,说明当前窗口小了,需要移动右窗口直到满足sum-x的状态。
class Solution {
public int minOperations(int[] nums, int x) {
int sum = 0;
int len = nums.length;
// 计算出数组总和
for (int i = 0; i < nums.length; i++) {
sum += nums[i];
}
int l = 0, r = 0;
int res = Integer.MAX_VALUE;
int times = 0;
while (l < len) {
// 调整右窗口大小
while (r < len) {
if (times + x >= sum) {
break;
}
times += nums[r];
r++;
}
if (times + x == sum) {
res = Math.min(res,len - (r - l));
}
// 左窗口要向右移动,减去nums[l]
times -= nums[l];
l++;
if (r < l) {
break;
}
}
if (res == Integer.MAX_VALUE) {
return -1;
}
return res;
}
}