算法题打卡力扣第1658题:将 x 减到 0 的最小操作数(mid)

文章目录

    • 题目描述
    • 解法一:暴力解
    • [解法二:逆向思维 + 滑动窗口 (最优解)](#解法二:逆向思维 + 滑动窗口 (最优解))
    • 边界条件与错误处理
      • [1. targetSum < 0(x 大于总和)](#1. targetSum < 0(x 大于总和))
      • [2. targetSum == 0(需要移除所有元素)](#2. targetSum == 0(需要移除所有元素))
      • [3. 数组为空](#3. 数组为空)
      • [4. 无法凑成 x(maxLen 保持 -1)](#4. 无法凑成 x(maxLen 保持 -1))
      • [5. 边界情况汇总测试](#5. 边界情况汇总测试)

题目描述

提示:

1 <= nums.length <= 105

1 <= numsi <= 104

1 <= x <= 109

解法一:暴力解

解题思路:

最直观的想法是直接模拟操作。我们可以尝试移除左边的 i 个元素,然后看看需要从右边移除多少个元素 j 才能使和为 x。

具体步骤:

  1. 预计算数组的前缀和 prefixSum 和后缀和 suffixSum,这样可以 O(1) 查询。
  2. 外层循环 i 从 0 到 n (包括不移除任何左边元素的情况)。
  3. 对于每一个 i,计算左边移除 i 个元素的和 leftSum = prefixSumi
  4. 我们需要从右边移除元素的和为 rightSum = x - leftSum。
  5. 在后缀和中查找是否存在一个 j,使得移除 j 个元素的和等于 rightSum。为了快速查找,可以预先将后缀和及其对应的长度存入一个哈希映射中。
  6. 如果找到了,总操作数就是 i + j。我们用一个变量来记录和更新最小的操作数。
  7. 需要注意 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 哨兵值得到了正确处理,保证了算法在各种输入下的健壮性。

相关推荐
传奇开心果编程1 小时前
【ArkUI 练中学】第1课:从零开始
学习·华为·前端框架
j7~1 小时前
【C++微服务项目开发脚手架】(准备篇)从 0 到跑通:Docker 容器开发环境 + 宿主机用户 + Trae 远程连接 全链路踩坑实战
linux·c++·学习·xshell·docker容器·trae
Gyr01 小时前
浅刷了一遍51CTO软考题库客观题及其解析(5)
学习·软考·知识点·信息安全工程师
Cccp.1231 小时前
【leetcode】(六) 图和贪心算法
数据结构·算法·leetcode
y1su1 小时前
【Leetcode】1477. 找两个和为目标值且不重叠的子数组
数据结构·后端·算法·leetcode·职场和发展
日拱一卒的小田11 小时前
ZYNQ学习笔记4-ZYNQ的SD卡控制器1
笔记·学习
mmmmath_312 小时前
LeetCode.541.反转字符串II
数据结构·算法·leetcode
Navigator_Z12 小时前
LeetCode //MySQL - 1251. Average Selling Price
c语言·算法·leetcode
拼图20913 小时前
Git常用语法
分布式·git·学习