[前端]_一起刷leetcode 1658. 将 x 减到 0 的最小操作数

245 阅读4分钟

这是我参与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 <= 105
  • 1 <= nums[i] <= 104
  • 1 <= x <= 109

思路

  1. 我们先分析一下这道题目存在的两种可能的结果:

    • 不存在这样子累加的一个x值

    那么这里又存在着两种可能,一种是前后两个片段的值加起来都超过当前这个x值,另一种是所有的元素加起来都比x值小。

    • 存在这样子累加的一个x值

    存在的时候有三种可能,一种是全部在前面,一种是全部在后面,一种是两边都有一部分。那么对于这种情况,我们尽可能统一处理。

  2. 我们可以根据小于x值的和的片段,对前后数组进行一个截取,要求截取出来的前后两个片段都是小于x值的。

  3. 然后我们把截取完的后面的片段,数组的后面连接上前面的片段,这个时候整一截数组就连在一起了,这时候我们只需要遍历一下当前的数组取到值相等的最大值即可,可以利用滑动窗口的思想来实现,就是不够的时候一直往右边移动,多了的时候减掉左边的部分,直到整个滑动窗口走完时返回结果即可。

实现

/**
 * @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;
        }
    }
}

结果

image.png

结果虽然通过了,但是这个用时比没通过更让我难受,所以我们顺着刚刚的思路往下做优化,看看怎么提高性能。

优化思路

我们可以给整个数组求和得到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;
};

看懂了的小伙伴可以点个关注、咱们下道题目见。如无意外以后文章都会以这种形式,有好的建议欢迎评论区留言。