#LC1658. 将 x 减到 0 的最小操作数

将 x 减到 0 的最小操作数

你好,同学。今天我们要挑战的是一道非常经典的数组双指针/滑动窗口问题。这道题在 NOI 普及组(入门级)或提高组的初级阶段经常出现,它考察了选手“转换问题视角”的能力。

很多同学第一眼看到“从左端或右端移除元素”,会想到深搜(DFS)或动态规划(DP),但如果看到数据范围 n105n \le 10^5,我们就必须寻找更高效的线性 O(n)O(n) 解法。


一、 预备知识点

  1. 正数数组的性质:所有元素均为正整数,这意味着前缀和是严格单调递增的。
  2. 补集转化思想:将“寻找两端之和为 xx 的最小长度”转化为“寻找中间连续子数组之和为 targettarget 的最大长度”。
  3. 滑动窗口(双指针):利用单调性,动态维护一段和为定值的区间。

二、 题目描述 (NOI 竞赛风格)

题目名称:将 x 减到 0 的最小操作数 (Minimum Operations to Reduce x to Zero)

【问题描述】 给定一个整数数组 nums 和一个整数 xx。每一次操作时,你应当移除数组 nums 最左边或最右边的元素,然后从 xx 中减去该元素的值。请注意,你修改的是原数组,影响随后的操作。 你的目标是通过 最小次数 的操作,将 xx 恰好 减到 00。如果无法做到,输出 1-1

【输入格式】 第一行包含两个正整数 nnxx,分别表示数组的长度和目标值。 第二行包含 nn 个以空格分隔的正整数,表示数组 nums

【输出格式】 输出一个整数,表示将 xx 减到 00 的最小操作次数。如果不存在方案,输出 1-1

【样例 1 输入】

5 5
1 1 4 2 3

【样例 1 输出】

2

(解释:最佳方案是移除右侧的 3 和 2,共 2 次操作)

【样例 2 输入】

3 10
5 6 7

【样例 2 输出】

-1

【数据规模与约定】

  • 1n1051 \le n \le 10^5
  • 1nums[i]1041 \le nums[i] \le 10^4
  • 1x1091 \le x \le 10^9

三 : 启发式引导:草稿纸上的推理过程

请拿起笔,跟着我一步步分析:

第一步:逆向思维 (视角转换)

题目要求:左边切掉一段 + 右边切掉一段 = xx思考: 剩下的中间那一段是什么? 剩下的中间部分是一个连续子数组。如果总和是 totalSum,那么中间那一块的和必须是:

target=totalSumxtarget = totalSum - x

第二步:目标重定义

  • 原目标:两端移除的元素最少。
  • 新目标:中间剩下的连续子数组长度最长。 这就变成了一个经典的“和为定值的最长子数组”问题。

第三步:画图模拟滑动窗口

假设 target = 5,数组为 [1, 1, 4, 2, 3]

  1. 设置 left = 0, right = 0,当前窗口和 currentSum = 0
  2. 移动 right,把元素拉进窗口:
    • [1], [1, 1], [1, 1, 4] (此时和为 6,大于 5 了!)。
  3. currentSum > target 时,移动 left 缩小窗口:
    • 移除最左边的 1,变成 [1, 4],此时和恰好为 5。
  4. 记录当前长度 right - left + 1,并更新全局最大长度。

第四步:时间和空间复杂度分析

  • 时间复杂度:每个元素最多被 right 指针扫描一次,被 left 指针移出一次。总复杂度为 O(n)O(n)
  • 空间复杂度:只需存储原数组,空间复杂度为 O(n)O(n)。在 NOI 竞赛中,10510^5int 数组仅占约 0.4MB,远低于内存限制。

四、 NOI 风格 C++14 伪代码提示

#include <iostream>
#include <vector>
#include <numeric> // 用于求和
#include <algorithm>

使用命名空间 标准库;

// 全局变量定义
整型 数组[100005];

函数 解决() {
    长长整型 总和 = 0;
    读取 n, x;
    对于 i 从 0 到 n-1 {
        读取 数组[i];
        总和 += 数组[i];
    }

    // 目标和:中间剩下的部分
    长长整型 目标值 = 总和 - x;

    // 边界情况判定
    如果 (目标值 < 0) { 输出 -1; 返回; }
    如果 (目标值 == 0) { 输出 n; 返回; }

    整型 左指针 = 0, 当前窗口和 = 0, 最大长度 = -1;

    对于 右指针 从 0 到 n-1 {
        当前窗口和 += 数组[右指针];

        // 窗口和太大,左边收缩
        当 (左指针 <= 右指针 且 当前窗口和 > 目标值) {
            当前窗口和 -= 数组[左指针];
            左指针++;
        }

        // 找到符合条件的区间
        如果 (当前窗口和 == 目标值) {
            最大长度 = 取最大值(最大长度, 右指针 - 左指针 + 1);
        }
    }

    // 最终答案是 总长度 - 最长中间段长度
    如果 (最大长度 == -1) 输出 -1;
    否则 输出 n - 最大长度;
}

五、 总结:读题关键词与优化建议

读题关键词:

  1. “最小次数” + “数组两端移除”:这类题通常可以转化为中间连续部分的最大长度
  2. “正整数”:这是使用滑动窗口的关键信号!如果数组里有负数,区间和就不再具有单调性,必须使用前缀和+哈希表(Map)来解决。

优化建议:

  • 数据溢出:注意 xx 可能很大,虽然 nums[i] 之和在 n=105n=10^5 时可能达到 10910^9(仍在 int 范围),但为了保险,求和变量建议使用 long long
  • 无法达成的情况:如果 totalSum < x,直接输出 -1,避免无效计算。
  • IO 优化:NOI 比赛中,处理 10510^5 级别数据建议添加 ios::sync_with_stdio(false); cin.tie(0);

教练点评: 这道题的难点不在于代码实现,而在于数学模型的转化。在草稿纸上画出“总长 = 两端 + 中间”的关系图,比直接写代码重要得多。这种“正难则反”的转化技巧,是金牌选手的基本功。加油!