👋 欢迎阅读

一.题目
1658. 将 x 减到 0 的最小操作数 - 力扣(LeetCode)
🎯 欢迎来到「将 x 减到 0 的最小操作数」题解之旅! 本文将带你从"从数组两端移除元素使总和恰好为 x"这一逆向思维问题出发,深入理解滑动窗口 的巧妙转化,并掌握如何通过寻找最长连续子数组来间接求解最少操作次数。
在开始之前,建议你先:
-
了解题目背景 :这是 LeetCode 1658 题,给定数组
nums和整数x,每次只能移除最左或最右的元素,并从x中减去该值,求使x恰好减为 0 的最少操作数 ;若无法实现则返回-1。直接模拟两端移除较复杂,但若逆向思考------保留下来的必定是数组中间的一段连续子数组 ,其和等于总和 - x,问题就转化为找最长的连续子数组 ,使得其和等于target = sum - x,而最少操作数就是n - 最长子数组长度。 -
明确学习目标 :掌握逆向转化技巧 ------将"移除两端"转化为"保留中间连续段",并用滑动窗口 在 O(n)O(n) 时间内找到和为目标值的最长子数组。理解为什么"窗口和等于 target "时就是合法解,以及如何通过收缩左边界 处理窗口和过大的情况,同时熟练处理边界(如
target < 0或不存在则返回-1)。 -
准备好环境 :建议在本地 IDE 或 LeetCode 在线编辑器中打开代码,边看边运行,亲手验证示例(如
nums = [1,1,4,2,3], x = 5输出2)。
本文将从问题转化、逆向构造目标、滑动窗口实现 到代码剖析,层层递进。即使你对滑动窗口还不熟悉,我们也会从"移除两端其实就是保留中间"这一核心直觉出发,让你轻松抓住关键思想 ------最少的操作数对应最长的中间连续子数组,而滑动窗口正是寻找满足固定和的最长窗口的标准工具。现在,让我们一起用逆向思维,把难题转化为熟悉的子数组和问题吧! 🔄📐
二.做题思路
一、问题分析(前置分析)
每次从数组 最左端或最右端 移除一个元素,并将其值从 x 中减去,直到 x 恰好变为 0。求 最少操作次数 。
核心转化(逆向思维) :从两端移除元素,等价于在数组中间 保留一个连续子数组 ,该子数组的和 = 数组总和 - x 。
于是问题变为:在数组中寻找一个和恰好等于 target = sum - x 的最长连续子数组 ,因为操作次数 = n - 子数组长度,子数组越长,操作越少。
若不存在这样的子数组,或 target < 0,返回 -1。
二、算法策略(滑动窗口)
-
先计算 数组总和
sum1。若sum1 == x,直接返回n(移除整个数组)。 -
令
target = sum1 - x,若target < 0,直接返回 -1。 -
使用 滑动窗口 在数组上寻找 和恰好等于
target的最长连续子数组:-
左右指针
left、right初始为 0,变量sum2记录当前窗口和。 -
右指针
right向右扩展,将nums[right]加入sum2。 -
若
sum2 > target,则 收缩左指针left,直到sum2 <= target。 -
若
sum2 == target,则找到一个有效子数组,更新 最少操作次数len = min(len, n - (right - left + 1))。
-
-
遍历结束后,若
len仍为初始最大值,返回 -1;否则返回len。
示例执行过程 (nums = [1, 1, 4, 2, 3], x = 5,总和 sum1 = 11,target = 6):
| 步骤 | right |
nums[right] |
入窗后 sum2 |
sum2 > target? |
收缩后 sum2 |
窗口 [left, right] |
长度 | 操作次数 n - len |
更新 len |
|---|---|---|---|---|---|---|---|---|---|
| 初始 | - | - | 0 | - | - | - | - | - | ∞ |
| 1 | 0 | 1 | 1 | 否 | 1 | 0,0 | 1 | 4 | ∞ |
| 2 | 1 | 1 | 2 | 否 | 2 | 0,1 | 2 | 3 | ∞ |
| 3 | 2 | 4 | 6 | 否(等于) | 6 | 0,2 | 3 | 2 | 2 |
| 4 | 3 | 2 | 8 | 是 | 收缩后 sum2=6,left=2 |
2,3 | 2 | 3 | 2(保持) |
| 5 | 4 | 3 | 9 | 是 | 收缩后 sum2=5,left=3 |
3,4 | 2 | 3 | 2(保持) |
三、正确性说明(简单版本)
逆向转化保证了原问题的解与 寻找和为 target 的连续子数组 等价。滑动窗口 在正数数组上能找到所有和为 target 的连续子数组,因为当窗口和超过目标时,所有正数都只会增加和,所以必须收缩左指针。在窗口和等于目标时,记录当前窗口长度,取最大值。由于我们记录的是 最长 有效子数组长度,所以操作次数 n - 最长长度 就是 最少 操作次数。该策略遍历了所有可能的连续子数组,不会漏解,因此正确。
四、实现细节(边界防护)
-
先计算 总和
sum1,若sum1 == x直接返回n。 -
计算 目标值
target = sum1 - x,若target < 0返回-1。 -
初始化
left = 0,sum2 = 0,len = INT_MAX。 -
使用
for循环 遍历右指针right:-
sum2 += nums[right]入窗口; -
若
sum2 > target,则 收缩左指针 :while (sum2 > target && left <= right) { sum2 -= nums[left++]; } -
若
sum2 == target,更新len = min(len, n - (right - left + 1))。
-
-
注意
while循环条件 应使用sum2 > target && left <= right防止left越界(原代码用left < right,但建议改用更严谨的写法)。 -
若遍历结束后
len == INT_MAX,返回-1;否则返回len。 -
时间复杂度 O(n) ,空间复杂度 O(1)。
五、返回值(目标映射)
返回 len,即 将 x 减到 0 所需的最少操作数。若无法实现,返回 -1。
三.代码
cpp
class Solution
{
public:
int minOperations(vector<int>& nums, int x)
{
// 算法思路:逆向思维
// 题目要求从数组两端移除元素,使得移除的元素之和等于 x。
// 这等价于:在数组中间保留一个连续子数组,其和 = 数组总和 - x。
// 问题转化为:寻找最长连续子数组,使其和为 target = sum - x。
// 然后用数组总长度 n 减去该子数组长度,即为最少操作次数。
// 若 target < 0 或不存在这样的子数组,则返回 -1。
int n = nums.size();
// 1. 计算数组总和 sum1
int sum1 = 0; // 数组所有元素之和
for (int i = 0; i < n; i++)
{
sum1 += nums[i];
}
// 特殊情况:如果整个数组的和正好等于 x,则只需移除整个数组,操作次数为 n
if (sum1 == x)
{
return n;
}
// 如果 x 为负数(题目不会出现,但为健壮性处理),返回 -1
if (x < 0)
{
return -1;
}
// 目标值:需要保留的连续子数组的和
int target = sum1 - x;
int sum2 = 0; // 当前窗口内元素的和
int len = INT_MAX; // 记录最少操作次数(初始化为最大值)
int left = 0, right = 0; // 滑动窗口的左右指针
// 2. 滑动窗口:寻找和为 target 的最长连续子数组
// 右指针不断向右扩展,累加元素到 sum2
for (right = 0; right < n; right++)
{
sum2 += nums[right]; // 入窗口
// 当窗口内和大于 target 时,需要收缩左边界,直到 sum2 <= target
// 注意:left < right 防止 left 越界(当 left == right 且 sum2 > target 时,会先收缩 left)
while (left < right && target < sum2)
{
sum2 -= nums[left]; // 出窗口
left++; // 左指针右移
}
// 如果窗口内和恰好等于 target,则说明找到了一个有效的连续子数组
// 其长度为 right - left + 1,对应的操作次数为 n - 子数组长度
if (target == sum2)
{
// 更新最少操作次数(保留的子数组越长,操作次数越少)
len = min(len, n - (right - left + 1));
}
}
// 3. 如果 len 仍为 INT_MAX,说明不存在和为 target 的子数组,返回 -1
if (len == INT_MAX)
{
return -1;
}
// 否则返回最少操作次数
return len;
}
};
四、易错点分析
难点一:问题转化------为什么用"保留子数组"代替"两端移除"?
int target = sum1 - x;
核心难点:
题目要求从数组两端移除元素,使其和恰好等于
x,求最少操作数。直接模拟两端移除非常复杂。逆向思维 :移除的元素和等于
x,那么未被移除的中间连续部分的和 必然等于sum - x。因此问题转化为:在数组中找到和最接近或恰好等于target的连续子数组,且我们想要这个子数组尽可能长 (因为剩余要移除的元素数量 =n - 子数组长度,越长操作越少)。难点在于从"两端取"自然过渡到"中间留",需要明确理解
target的含义以及为什么保留子数组越长越好。
难点二:滑动窗口维护的目标------是找"和等于 target"的窗口,而非"不超过 target"
while (left < right && target < sum2)
{
sum2 -= nums[left];
left++;
}
if (target == sum2)
{
len = min(len, n - (right - left + 1));
}
理解上的障碍:
常规滑动窗口通常用于找"和 ≥ target"或"和 ≤ target"的最长/最短子数组。而本代码中,
while收缩条件target < sum2只是为了保证窗口和不大于target,但不会继续收缩到sum2 < target,因为元素都是正数,一旦sum2降到 ≤target,就可能等于或小于。关键 :收缩后,我们只关心
sum2 == target的情况,而不关注小于 target 的情况(因为小于 target 无法通过扩大右边界再恢复?实际上,右边界继续扩展后可能再次等于 target)。这要求理解每次右边界固定时,我们通过收缩左边界得到当前右端点下的最小合法窗口(和 ≤ target) ,然后判断是否恰好等于 target,从而记录有效解。难点在于区分"收缩过程"和"判断相等"是两个独立步骤,并且
while条件里用了target < sum2而不是sum2 > target(等价),但隐含了sum2可能小于 target 的情况会被保留,等待后续扩展。
难点三:用 min 更新 len 等价于寻找最长保留子数组
len = min(len, n - (right - left + 1));
容易产生的困惑:
初始化
len = INT_MAX,每找到一个和等于target的窗口,就计算操作数n - 窗口长度,并取最小值。由于
n是固定的,n - 窗口长度越小,意味着窗口长度越大。因此多次取最小操作数等价于在所有可行窗口中取最大长度 。难点在于:代码没有显式地维护一个"最大窗口长度"变量,而是直接更新操作数,这需要理解两者之间的等价性。同时,因为滑动窗口可能找到多个重叠窗口,取最小操作数保证了全局最优。
五、流程图

🎯 闭幕

🎉 恭喜你完成了「将 x 减到 0 的最小操作数」问题的学习!
为了巩固知识并进一步拓展,建议你:
🚀 动手实践
在 LeetCode 上提交代码,尝试不同的测试用例。
💡 深入思考
-
本题采用 逆向思维 ,将"从两端移除元素"转化为 寻找中间连续子数组 ,使该子数组和为
sum - x。请问 为什么这样转化是等价的?其数学依据是什么? -
代码中滑动窗口寻找的是 和为 target 的最长连续子数组 ,而不是直接求最小操作数。为什么最长子数组对应最少操作数?
-
当
target小于 0 时,直接返回 -1,这是否合理?如果sum1 == x时返回n,这种情况对应什么场景? -
数组元素均为 正整数 ,这对滑动窗口的有效性至关重要。如果数组中存在 负数,当前算法还正确吗?为什么?
📚 延伸挑战
-
如果题目要求 输出具体移除的方案(例如最左边移除几个,最右边移除几个),而不是只返回操作次数,你的代码应如何改造以记录方案?
-
如果将问题改为 "将 x 减到 0 的最大操作数"(即尽量多移除元素,但最后仍要恰好减到 0),算法应如何调整?
如果你觉得本文对你有所帮助,欢迎:
👍 点赞 / 收藏
👤 关注作者 ,获取更多题解
💬 留言交流你的疑问或优化思路
📌 深入思考答案
-
转化依据 :数组两端的移除元素之和等于 x,等价于 剩余中间连续部分的元素之和 = 总和 - x 。因为移除操作是从两端不断剥离,最后剩下的必然是一个连续子数组,所以原问题转为 找到和为 target 的最长连续子数组。
-
最长子数组对应最少操作数:因为操作数 = 总元素个数 - 保留的子数组长度,保留得越长,移除的元素越少,操作数越少。因此求最长保留长度即可得到最少操作数。
-
target < 0时(即 x > 总和),永远无法减到 0,返回 -1 正确;sum1 == x时,保留子数组长度为 0(即移除全部元素),操作数为 n,代码直接返回 n 是正确的最优解。 -
存在负数时滑动窗口失效 ,因为窗口和不再单调(增加负数会减少和),收缩条件
sum2 > target不再能保证通过左移来调整,必须使用前缀和 + 哈希或双端队列等更复杂方法,因此本题限定正整数是必要的。
🔍 延伸挑战答案
-
挑战1:在滑动窗口找到和为 target 的最长子数组后,记录其起始和结束下标,则移除的左端个数 = 起始下标,右端个数 = n - 结束下标 - 1,即可输出具体方案。
-
挑战2 :若要求最大操作数,则需 保留最短的连续子数组(和仍为 target),即求最短的满足和为 target 的连续子数组长度,然后用 n 减去该长度得到最大操作数;只需将求最长改为求最短即可(记录最小长度)。
祝你在 算法之路 上越走越稳,早日攻克每一道难题!下次见 🚀✨