这是我参与2022首次更文挑战的第14天,活动详情查看:2022首次更文挑战
题目
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 <= nums.length <= 1051 <= nums[i] <= 1041 <= x <= 109
思路
-
我们先分析一下这道题目存在的两种可能的结果:
- 不存在这样子累加的一个x值
那么这里又存在着两种可能,一种是前后两个片段的值加起来都超过当前这个x值,另一种是所有的元素加起来都比x值小。
- 存在这样子累加的一个x值
存在的时候有三种可能,一种是全部在前面,一种是全部在后面,一种是两边都有一部分。那么对于这种情况,我们尽可能统一处理。
-
我们可以根据小于x值的和的片段,对前后数组进行一个截取,要求截取出来的前后两个片段都是小于x值的。
-
然后我们把截取完的后面的片段,数组的后面连接上前面的片段,这个时候整一截数组就连在一起了,这时候我们只需要遍历一下当前的数组取到值相等的最大值即可,可以利用滑动窗口的思想来实现,就是不够的时候一直往右边移动,多了的时候减掉左边的部分,直到整个滑动窗口走完时返回结果即可。
实现
/**
* @param {number[]} nums
* @param {number} x
* @return {number}
*/
var minOperations = function(nums, x) {
const prevArr = getLessTargetArray(nums, x);
if (!prevArr) return -1;
const lastArr = getLessTargetArray2(nums, x);
// 如果两边都没符合的,直接返回
if (!prevArr.length && !lastArr.length) return -1;
let queue = [...lastArr, ...prevArr];
// 如果两边只有一个返回的。直接判断和即可
if (!prevArr.length || !lastArr.length) {
return queue.reduce((total, cur) => total += cur) === x ? prevArr.length || lastArr.length : -1;
}
const n = queue.length;
// 构建前缀和树
const prevSubTree = new Array(n + 1).fill(0);
for (let i = 0; i < n; i++) {
prevSubTree[i + 1] = prevSubTree[i] + queue[i];
}
let slow = 0,
fast = 0,
minCount;
while (fast < queue.length) {
let total = prevSubTree[fast + 1] - prevSubTree[slow];
if (total === x) {
if (minCount) {
minCount = Math.min(minCount, fast - slow + 1);
} else {
minCount = fast - slow + 1;
}
slow++;
fast++;
} else if (total > x) {
slow++;
} else {
fast++;
}
}
return minCount || -1;
};
// 获取当前数组小于目标值的前缀集合
function getLessTargetArray(nums, x) {
let result = [],
total = 0;
for (let i = 0; i < nums.length; i++) {
total += nums[i];
result.push(nums[i]);
// 多了切掉
if (total > x) {
result.pop();
return result;
} else if (total === x) {
return result;
}
}
}
// 获取当前数组小于目标值的前缀集合 -- 倒序的
function getLessTargetArray2(nums, x) {
let result = [],
total = 0;
for (let i = nums.length - 1; i >= 0; i--) {
total += nums[i];
result.unshift(nums[i]);
// 多了切掉
if (total > x) {
result.shift();
return result;
} else if (total === x) {
return result;
}
}
}
结果
结果虽然通过了,但是这个用时比没通过更让我难受,所以我们顺着刚刚的思路往下做优化,看看怎么提高性能。
优化思路
我们可以给整个数组求和得到sum,求和完找出sum - x的值target,然后我们只需要找到中间片段的和为target的片段的长度最长的即可。
优化代码
/**
* @param {number[]} nums
* @param {number} x
* @return {number}
*/
var minOperations = function(nums, x) {
const n = nums.length;
// 对整个数组进行求和
let sum = nums.reduce((total, cur) => total += cur);
// 如果和比目标值小就不用找了
if (sum < x) return -1;
// 构建前缀和树
const prevSubTree = new Array(n + 1).fill(0);
for (let i = 0; i < n; i++) {
prevSubTree[i + 1] = prevSubTree[i] + nums[i];
}
// 目标值和快慢指针的位置
let target = sum - x;
slow = 0,
fast = 0,
maxCount = -1; // 现在求的是中间可以删除的片段,越长越好
while (fast < n) {
let total = prevSubTree[fast + 1] - prevSubTree[slow];
if (total === target) {
maxCount = Math.max(maxCount, fast - slow + 1);
slow++;
fast++;
} else if (total > target) {
slow++;
} else {
fast++;
}
}
return maxCount === -1 ? -1 : n - maxCount;
};
看懂了的小伙伴可以点个关注、咱们下道题目见。如无意外以后文章都会以这种形式,有好的建议欢迎评论区留言。