LeetCode 1658 最小操作数减 X 到零:Go 实现滑动窗口逆向思维全解析
【免费下载链接】LeetCode-Go✅ Solutions to LeetCode by Go, 100% test coverage, runtime beats 100% | LeetCode 题解项目地址: https://gitcode.com/GitHub_Trending/le/LeetCode-Go
导读
LeetCode 1658 是一道经典的"两端取数"问题:每次操作只能从数组最左端或最右端移除一个元素并从x中减去其值,求恰好将x减到 0 所需的最小操作数。本文以 LeetCode-Go 仓库中 1658.Minimum-Operations-to-Reduce-X-to-Zero 的官方题解为骨架,完整还原其核心解题思路——把"两端的数字最少"逆向转化为"中间连续子数组最长",并给出可直接运行的 Go 源码、测试用例与复杂度分析。读完本文,你将掌握这类"两端操作"问题通用的滑动窗口转化技巧。
题目理解
给定一个整数数组nums和一个整数x,每一次操作时,应移除数组nums最左边或最右边的元素,然后从x中减去该元素的值,并且需要修改数组以供接下来的操作使用。如果可以将x恰好减到 0,返回最小操作数;否则返回-1。
原文档给出了三个示例:
示例 1: Input: nums = [1,1,4,2,3], x = 5 Output: 2 解释:最优解是移除最后两个元素,即可把 x 减到 0。 示例 2: Input: nums = [5,6,7,8,9], x = 4 Output: -1 示例 3: Input: nums = [3,2,20,1,1,3], x = 10 Output: 5 解释:最优解是移除最后三个元素和前两个元素(共 5 次操作),即可把 x 减到 0。数据约束(原文档 Constraints):
1 <= nums.length <= 10^51 <= nums[i] <= 10^41 <= x <= 10^9
题目大意是:从数组两端分别移除一些数,使得这些被移除的数加起来正好等于整数x,要求输出最小操作数,否则返回-1。
核心思路:逆向转化为"最长连续子数组"
这是本题最关键的一步思维转换,也是原文档解题思路的核心:
- 要求输出最小操作数,即数组两头的数字个数最少,并且加起来和正好等于
x。 - 由于要操作的位置在数组的两头,直接用 2 个指针分别操作不太方便。原文档作者当时解题时的思路是把它变成循环数组,这样两边的指针就在一个区间内了,再利用滑动窗口找一个最小的窗口,使得窗口内累加和等于整数
x。这个方法可行,但代码较多。 - 更优美的做法:要想两头的长度最少,也就是中间这段的长度最大。这样就转换成直接在数组上使用滑动窗口求解:寻找累加和等于一个固定值的连续最长子数组。
具体而言,令数组总和为total,那么中间连续子数组的目标和为:
target = total - x- 若
target < 0:说明数组总和都小于x,无论怎么移除都无法减到 0,直接返回-1; - 若
target == 0:说明必须把整个数组全部移除,操作数就是len(nums); - 否则,用滑动窗口在
nums上寻找和为target的最长连续子数组,其长度为res,答案即为len(nums) - res。
这个逆向转化把"从两端取数"这种不便于双指针直接处理的场景,规约成了经典的"定和最长子数组"问题,时间复杂度由可能的指数级搜索降为一次线性扫描。
边界情况分析
在动手写代码前,先梳理清楚三类边界情况:
total < x:数组内所有元素之和都小于x,两端移除不可能凑出x,答案是-1。total == x:需要把整个数组全部移除,操作数为n(n为数组长度)。- 存在恰好等于
target的子数组:找到其中最长的那个,答案取n - 最长长度;若找不到任何和为target的连续子数组,则答案同样为-1。
这些边界分支在仓库源码 1658. Minimum Operations to Reduce X to Zero.go 的前半部分均有显式处理。
Go 源码实现逐行解析
仓库中的完整实现如下(与 README.md 中给出的代码一致):
package leetcode func minOperations(nums []int, x int) int { total := 0 for _, n := range nums { total += n } target := total - x if target < 0 { return -1 } if target == 0 { return len(nums) } left, right, sum, res := 0, 0, 0, -1 for right < len(nums) { if sum < target { sum += nums[right] right++ } for sum >= target { if sum == target { res = max(res, right-left) } sum -= nums[left] left++ } } if res == -1 { return -1 } return len(nums) - res } func max(a, b int) int { if a > b { return a } return b }关键步骤说明
- 第 3-7 行:先遍历一遍数组求出总和
total,得到中间子数组的目标和target = total - x。 - 第 8-13 行:处理两类边界情况——
target < 0直接返回-1;target == 0返回len(nums)。 - 第 15 行:初始化滑动窗口。
left、right为窗口左右边界(左闭右开),sum维护窗口内元素和,res记录和为target的最长窗口长度,初始为-1表示尚未找到。 - 第 16-19 行:当
sum < target时,右指针right向右扩张,把新元素纳入窗口。 - 第 20-26 行:当
sum >= target时进入内层循环:若sum == target则用max(res, right-left)更新最长长度;随后把左指针元素移出窗口(sum -= nums[left]; left++),继续收缩寻找更优解。 - 第 28-32 行:若始终没有找到和为
target的窗口(res == -1),返回-1;否则返回len(nums) - res,即需要移除的元素个数。
注意res = max(res, right-left)中right-left恰好是当前窗口长度(因为right已指向窗口右开边界),这也解释了为何源码在sum == target时直接以right-left参与比较。
复杂度分析
- 时间复杂度:
O(n)。数组只被完整遍历一次用于求和,滑动窗口的左右指针各至多移动n次,整体线性。 - 空间复杂度:
O(1)。仅使用若干整型变量,没有额外数组或哈希表。
该解法可以在一次线性扫描内同时完成"找定和窗口"与"求最长长度"两个目标,相比"构造循环数组 + 双指针"的朴素做法,代码量更少、边界更清晰。
测试用例与验证
仓库配套的测试文件 1658. Minimum Operations to Reduce X to Zero_test.go 使用表格驱动(table-driven)风格组织用例,完整覆盖了以下场景:
| nums | x | 期望输出 | 覆盖点 |
|---|---|---|---|
[1,1,4,2,3] | 5 | 2 | 原题示例 1,只移除右侧两个元素 |
[5,6,7,8,9] | 4 | -1 | 原题示例 2,无法凑出目标值 |
[3,2,20,1,1,3] | 10 | 5 | 原题示例 3,左右两侧都要移除 |
[1,1] | 5 | -1 | total < x的边界分支 |
[1,2,3] | 6 | 3 | total == x,需移除全部元素 |
其中最后两个用例对应本文前面分析的边界分支:[1,1]总和为 2 小于x=5,直接命中target < 0返回-1;[1,2,3]总和恰好等于x=6,命中target == 0返回len(nums) == 3。测试通过got != a.one判定失败并打印input/output,可以在仓库根目录执行go test ./leetcode/1658.Minimum-Operations-to-Reduce-X-to-Zero/ -v复现运行结果。
解法思路的延伸:同类题推荐
原文档在解题思路末尾明确给出了与本题思路相似的三道推荐题目:
- 209. Minimum Size Subarray Sum:同样是滑动窗口求解连续子数组和问题,不过本题求的是和≥ s的最短连续子数组,与 1658 的"定和最长"恰好互为镜像。本仓库已有完整实现,参见 209.Minimum-Size-Subarray-Sum;
- 1040. Moving Stones Until Consecutive II:涉及循环数组与"端点移动"限制的思维题,其最小步数求解同样借助了定长滑动窗口,参见 1040.Moving-Stones-Until-Consecutive-II;
- 325. Maximum Size Subarray Sum Equals k:与 1658 的逆向转化几乎同构——求"和为固定值的最长连续子数组",区别在于 325 允许负数(需借助哈希表维护前缀和),而 1658 因数组全为正数可直接用滑动窗口,本题在 LeetCode 上需要付费订阅查看。
从源码结构可以推断,本仓库将 209 与 1040 的题解(含_test.go测试文件)以同样的目录规范组织,便于横向对比学习这三道题在滑动窗口运用上的异同。
总结
LeetCode 1658 的精髓在于逆向思维:与其直接模拟"从两端移除元素"的过程,不如把问题等价地改写为"找到和为total - x的最长连续子数组",再用一次线性滑动窗口求解。本仓库的 Go 实现仅用约 30 行代码即完成全部逻辑,并通过表格驱动测试覆盖了原题示例与边界分支。掌握这一转化模式后,无论是本题,还是 209、1040、325 等同类滑动窗口题目,都可以举一反三、快速定位解法。
【免费下载链接】LeetCode-Go✅ Solutions to LeetCode by Go, 100% test coverage, runtime beats 100% | LeetCode 题解项目地址: https://gitcode.com/GitHub_Trending/le/LeetCode-Go
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考