返回

洞悉滑动窗口、前缀和与二分的巧妙结合:在 JavaScript 中化解 LeetCode 1658

前端

踏入 LeetCode 1658 的难题之门

LeetCode 1658 题意直白而引人入胜:给定一个整数数组 nums,其中每个元素都代表一种操作。具体而言,你可以对数组执行两种操作:

  1. 将数组中的任意一个元素减 1。
  2. 将数组中的任意两个相邻元素互换。

你的目标是找出将数组中的所有元素都减到 0 所需的最小操作次数。

乍一看,这个难题似乎有些棘手,但不要气馁,掌握了正确的算法技巧,你将所向披靡。

滑动窗口:划定可控范围

滑动窗口算法就像一把锋利的双刃剑,它可以动态地遍历数组,在有限的窗口内执行操作。在本题中,滑动窗口将帮助我们聚焦于当前需要处理的元素。

我们从窗口的左端开始,将窗口向右滑动,同时执行以下步骤:

  1. 计算当前窗口内元素的和 :这将告诉我们窗口内有多少个元素需要减到 0。
  2. 计算窗口内最大元素 :这将告诉我们窗口内最需要优先减小的元素。

前缀和:快速查询元素之和

前缀和是一种预处理技术,它可以快速计算数组中任意连续元素的和。在本题中,我们利用前缀和来计算滑动窗口内元素的和。

具体而言,我们创建一个前缀和数组 prefix,其中 prefix[i] 表示数组 nums 中前 i 个元素的和。有了前缀和数组,我们就可以使用常数时间复杂度 O(1) 计算任意滑动窗口内元素的和。

二分查找:精准定位最小值

二分查找是一种高效的搜索算法,它可以快速找到有序数组中的目标元素。在本题中,我们将利用二分查找来寻找数组 nums 中的最小元素。

为什么需要寻找最小元素呢?因为在任何给定的时刻,我们需要优先减小数组中最小的元素。这将确保我们在每次操作中获得最大的收益。

三剑合璧:算法的优雅舞步

现在,让我们将这三种算法利器巧妙地融合在一起:

  1. 滑动窗口 :动态地遍历数组,聚焦于当前需要处理的元素。
  2. 前缀和 :快速计算滑动窗口内元素的和。
  3. 二分查找 :精准地找到数组中的最小元素。

有了这些算法的加持,我们可以设计出一个高效的算法来解决 LeetCode 1658:

  1. 初始化前缀和数组 prefix
  2. 初始化滑动窗口的左端指针 left 和右端指针 right,并计算初始窗口的和。
  3. 进入循环,执行以下步骤:
    • 计算当前窗口的最小元素 min
    • 使用二分查找找到数组中第一个大于或等于 min 的元素。
    • 更新滑动窗口的左端指针 left 为找到的元素的索引。
    • 计算新窗口的和。
    • 更新最小操作次数 count
  4. 退出循环,返回 count

代码实现:JavaScript 的算法之美

掌握了算法的精髓,让我们将其转化为优雅的 JavaScript 代码:

/**
 * LeetCode 1658. 将 x 减到 0 的最小操作数
 *
 * @param {number[]} nums
 * @return {number}
 */
const minOperations = function (nums) {
    // 初始化前缀和数组
    const prefix = [0];
    for (let i = 0; i < nums.length; i++) {
        prefix.push(prefix[i] + nums[i]);
    }

    // 初始化滑动窗口
    let left = 0;
    let right = 0;
    let sum = 0;

    // 初始化最小操作次数
    let count = 0;

    while (right < nums.length) {
        // 计算当前窗口的最小元素
        let min = nums[right];
        for (let i = left; i <= right; i++) {
            min = Math.min(min, nums[i]);
        }

        // 使用二分查找找到数组中第一个大于或等于 min 的元素
        let index = binarySearch(nums, min);

        // 更新滑动窗口的左端指针
        left = index;

        // 计算新窗口的和
        sum = prefix[right + 1] - prefix[left];

        // 更新最小操作次数
        count += Math.ceil(sum / min);

        // 移动右端指针
        right++;
    }

    return count;
};

// 二分查找函数
const binarySearch = function (nums, target) {
    let left = 0;
    let right = nums.length - 1;

    while (left <= right) {
        let mid = Math.floor((left + right) / 2);

        if (nums[mid] >= target) {
            right = mid - 1;
        } else {
            left = mid + 1;
        }
    }

    return left;
};

结语

通过巧妙地结合滑动窗口、前缀和和二分查找,我们征服了 LeetCode 1658 的挑战。这道难题不仅考验了我们的算法技能,更拓宽了我们解决复杂问题的思路。

掌握了这些算法利器,你将如虎添翼,在算法的世界中游刃有余。继续探索,不断挑战自己,算法的殿堂将为你敞开大门!