文章目录
题目描述

提示:
1 <= nums.length <= 105
1 <= numsi <= 104
1 <= x <= 109
解法一:暴力解
解题思路:
最直观的想法是直接模拟操作。我们可以尝试移除左边的 i 个元素,然后看看需要从右边移除多少个元素 j 才能使和为 x。
具体步骤:
- 预计算数组的前缀和 prefixSum 和后缀和 suffixSum,这样可以 O(1) 查询。
- 外层循环 i 从 0 到 n (包括不移除任何左边元素的情况)。
- 对于每一个 i,计算左边移除 i 个元素的和 leftSum = prefixSumi。
- 我们需要从右边移除元素的和为 rightSum = x - leftSum。
- 在后缀和中查找是否存在一个 j,使得移除 j 个元素的和等于 rightSum。为了快速查找,可以预先将后缀和及其对应的长度存入一个哈希映射中。
- 如果找到了,总操作数就是 i + j。我们用一个变量来记录和更新最小的操作数。
- 需要注意 i+j 不能超过数组总长度 n。
代码实现
python
def minOperations(nums, x):
n = len(nums)
# 预计算前缀和
prefixSum = [0] * (n + 1)
for i in range(n):
prefixSum[i + 1] = prefixSum[i] + nums[i]
# 预计算后缀和
suffixSum = [0] * (n + 1)
for i in range(n - 1, -1, -1):
suffixSum[i] = suffixSum[i + 1] + nums[i]
# 将后缀和及其对应的移除个数存入哈希映射
suffixMap = {}
for j in range(n + 1):
suffixMap[suffixSum[j]] = j
ans = float('inf')
# 枚举左边移除 i 个元素
for i in range(n + 1):
leftSum = prefixSum[i]
rightSum = x - leftSum
if rightSum in suffixMap:
j = suffixMap[rightSum]
if i + j <= n:
ans = min(ans, i + j)
return -1 if ans == float('inf') else ans
# 示例测试
if __name__ == "__main__":
# 示例 1
nums1 = [1, 1, 4, 2, 3]
x1 = 5
print(minOperations(nums1, x1)) # 输出: 2
# 示例 2
nums2 = [5, 6, 7, 8, 9]
x2 = 4
print(minOperations(nums2, x2)) # 输出: -1
# 示例 3
nums3 = [3, 2, 20, 1, 1, 3]
x3 = 10
print(minOperations(nums3, x3)) # 输出: 5
执行结果
text
2
-1
5
复杂度分析:
时间:O(n)
空间:O(n)
解法二:逆向思维 + 滑动窗口 (最优解)
解题思路:
关键的思路转换 让我们换一个角度看问题: 题目要求我们找到一个最短的、由数组前缀和后缀组成的序列,其和为 x。
如果我们从数组中移除了一个前缀和一个后缀,剩下的是什么?是一个连续的、位于中间的子数组。 假设数组的总和是totalSum。如果我们移除了和为 x 的元素,那么剩下的中间子数组的和必然是 totalSum - x。
原问题:"找到和为 x 的最短前后缀"
新问题:"找到和为 totalSum - x 的最长连续子数组"
具体步骤:
- 计算目标和:
首先,计算整个数组的总和 totalSum。
计算我们中间子数组的目标和 targetSum = totalSum - x。 - 处理边界情况:
如果 targetSum < 0,说明 x 比总和还大,永远不可能凑成,直接返回 -1。
如果 targetSum == 0,这意味着我们需要移除所有元素才能使和为 x。此时,最长的中间子数组长度为 0,所以答案是 n (总操作数)。 - 应用滑动窗口:
初始化左指针 start = 0,当前窗口和 currentSum = 0。
初始化 maxLen = -1 (用于记录找到的最长子数组长度,-1 表示还没找到)。
使用右指针 end 从 0 遍历到 n-1:
a. 扩大窗口:currentSum += numsend。
b. 收缩窗口:当 currentSum > targetSum 时,我们需要从左边移出元素来缩小 sum。进入一个 while 循环,currentSum -= numsstart,然后 start++,直到 currentSum <= targetSum。
c. 检查匹配:在收缩窗口后,如果 currentSum == targetSum,说明我们找到了一个满足条件的中间子数组。我们用它的长度 end - start + 1 来更新 maxLen。 maxLen = max(maxLen, end - start + 1)。 - 计算最终结果:
滑动窗口结束后,如果 maxLen 仍然是 -1,说明从未找到和为 targetSum 的子数组,即无法凑成 x,返回 -1。
如果找到了 maxLen,那么最小操作数就是数组总长度 n 减去这个最长的保留子数组的长度。最终答案是 n - maxLen。
代码实现
代码实现
python
def minOperations(nums, x):
n = len(nums)
totalSum = sum(nums)
targetSum = totalSum - x
# 边界情况:x 比总和还大,无法凑成
if targetSum < 0:
return -1
# 边界情况:需要移除所有元素
if targetSum == 0:
return n
start = 0
currentSum = 0
maxLen = -1
# 滑动窗口
for end in range(n):
currentSum += nums[end]
# 收缩窗口
while currentSum > targetSum and start <= end:
currentSum -= nums[start]
start += 1
# 检查是否匹配
if currentSum == targetSum:
maxLen = max(maxLen, end - start + 1)
return -1 if maxLen == -1 else n - maxLen
# 示例测试
if __name__ == "__main__":
# 示例 1
nums1 = [1, 1, 4, 2, 3]
x1 = 5
print(minOperations(nums1, x1)) # 输出: 2
# 示例 2
nums2 = [5, 6, 7, 8, 9]
x2 = 4
print(minOperations(nums2, x2)) # 输出: -1
# 示例 3
nums3 = [3, 2, 20, 1, 1, 3]
x3 = 10
print(minOperations(nums3, x3)) # 输出: 5
执行结果
text
2
-1
5
复杂度分析:
时间:O(n)
空间:O(1)
边界条件与错误处理
滑动窗口解法虽然简洁,但正确性高度依赖几个边界条件的处理。下面逐一说明,并给出对应的测试用例。
1. targetSum < 0(x 大于总和)
当 x 比整个数组的总和 totalSum 还大时,targetSum = totalSum - x < 0,此时无论怎么移除,都不可能凑出和为 x 的前后缀,直接返回 -1。
python
# 测试用例:x 大于总和
nums = [1, 2, 3]
x = 10
print(minOperations(nums, x)) # 输出: -1
2. targetSum == 0(需要移除所有元素)
当 x 恰好等于 totalSum 时,targetSum = 0,意味着中间保留的子数组长度为 0,即需要把整个数组全部移除,操作数为 n。
python
# 测试用例:x 等于总和,需要移除全部元素
nums = [1, 2, 3]
x = 6
print(minOperations(nums, x)) # 输出: 3
3. 数组为空
虽然题目约束 1 <= nums.length,但作为健壮性考虑,若传入空数组:
- 当
x == 0时,无需任何操作,返回0; - 当
x > 0时,无法凑成,返回-1。
python
# 测试用例:空数组
print(minOperations([], 0)) # 输出: 0
print(minOperations([], 5)) # 输出: -1
4. 无法凑成 x(maxLen 保持 -1)
当 targetSum >= 0 但数组中不存在和为 targetSum 的连续子数组时,maxLen 始终为 -1,最终返回 -1。
python
# 测试用例:无法凑成 x
nums = [5, 6, 7, 8, 9]
x = 4
print(minOperations(nums, x)) # 输出: -1
5. 边界情况汇总测试
将上述用例整合成一段可运行的完整测试脚本:
python
def minOperations(nums, x):
n = len(nums)
totalSum = sum(nums)
targetSum = totalSum - x
if targetSum < 0:
return -1
if targetSum == 0:
return n
start = 0
currentSum = 0
maxLen = -1
for end in range(n):
currentSum += nums[end]
while currentSum > targetSum and start <= end:
currentSum -= nums[start]
start += 1
if currentSum == targetSum:
maxLen = max(maxLen, end - start + 1)
return -1 if maxLen == -1 else n - maxLen
# 边界测试
print(minOperations([1, 2, 3], 10)) # 输出: -1(x 大于总和)
print(minOperations([1, 2, 3], 6)) # 输出: 3(移除全部元素)
print(minOperations([], 0)) # 输出: 0(空数组,x=0)
print(minOperations([], 5)) # 输出: -1(空数组,x>0)
print(minOperations([5, 6, 7, 8, 9], 4)) # 输出: -1(无法凑成)
print(minOperations([1, 1, 4, 2, 3], 5)) # 输出: 2(常规用例)
以上所有边界情况都在代码中通过前置判断或滑动窗口的 maxLen 哨兵值得到了正确处理,保证了算法在各种输入下的健壮性。