2 条题解

  • 0
    @ 2025-12-19 23:12:39

    在构建 OJ(在线评测系统)的测试数据时,针对滑动窗口类题目,我们需要重点考察边界条件(如 xx 刚好等于总和)无解情况以及最大规模的随机数据

    以下是为你准备的数据生成器源码。它包含了标准逻辑并自动循环生成 10 组测试数据。

    数据生成器 (C++14)

    #include <iostream>
    #include <fstream>
    #include <vector>
    #include <string>
    #include <algorithm>
    #include <random>
    #include <chrono>
    #include <numeric>
    
    using namespace std;
    
    // --- 标程:用于根据输入生成 .out 文件 ---
    int getStandardOutput(int n, long long x, const vector<int>& nums) {
        long long totalSum = 0;
        for (int v : nums) totalSum += v;
        long long target = totalSum - x;
    
        if (target < 0) return -1;
        if (target == 0) return n;
    
        int maxLen = -1;
        long long currentSum = 0;
        int left = 0;
        for (int right = 0; right < n; ++right) {
            currentSum += nums[right];
            while (left <= right && currentSum > target) {
                currentSum -= nums[left];
                left++;
            }
            if (currentSum == target) {
                maxLen = max(maxLen, right - left + 1);
            }
        }
        return (maxLen == -1) ? -1 : (n - maxLen);
    }
    
    // --- 数据生成主逻辑 ---
    void make_data() {
        // 初始化高质量随机数引擎
        mt19937_64 rng(chrono::steady_clock::now().time_since_epoch().count());
    
        for (int t = 1; t <= 10; ++t) {
            int n;
            long long x;
            vector<int> nums;
    
            // --- 针对性构造不同类型的测试点 ---
            if (t == 1) { // 边界:最小规模,刚好达成
                n = 1; nums = {5}; x = 5;
            } 
            else if (t == 2) { // 边界:x 大于总和,必无解
                n = 5; nums = {1, 2, 3, 4, 5}; x = 20;
            }
            else if (t == 3) { // 边界:x 刚好等于总和
                n = 10; nums.resize(n, 100); x = 1000;
            }
            else if (t == 4) { // 特殊:中间一段非常大,导致 x 只能由两端极小值构成
                n = 100; nums.resize(n, 1);
                nums[50] = 10000; // 中间混入大数
                x = 10; // 目标值小
            }
            else if (t == 5) { // 随机中等规模:构造一个确定的解
                n = 1000; nums.resize(n);
                long long sum = 0;
                for(int i=0; i<n; ++i) { nums[i] = rng() % 1000 + 1; sum += nums[i]; }
                // 随机选取中间一段 [L, R] 留着,剩下的作为 x
                int L = rng() % 300, R = n - 1 - (rng() % 300);
                long long midSum = 0;
                for(int i=L; i<=R; ++i) midSum += nums[i];
                x = sum - midSum;
            }
            else if (t >= 6 && t <= 7) { // 随机大规模:可能无解
                n = 100000;
                nums.resize(n);
                for(int i=0; i<n; ++i) nums[i] = rng() % 10000 + 1;
                x = rng() % 1000000000 + 1;
            }
            else { // 最大规模:确保有解的构造
                n = 100000;
                nums.resize(n);
                long long sum = 0;
                for(int i=0; i<n; ++i) { nums[i] = rng() % 1000 + 1; sum += nums[i]; }
                int L = rng() % 10000, R = n - 1 - (rng() % 10000);
                long long midSum = 0;
                for(int i=L; i<=R; ++i) midSum += nums[i];
                x = sum - midSum;
            }
    
            // --- 写入 .in 文件 ---
            string in_name = to_string(t) + ".in";
            ofstream fin(in_name);
            fin << n << " " << x << "\n";
            for (int i = 0; i < n; ++i) {
                fin << nums[i] << (i == n - 1 ? "" : " ");
            }
            fin << "\n";
            fin.close();
    
            // --- 写入 .out 文件 ---
            string out_name = to_string(t) + ".out";
            ofstream fout(out_name);
            fout << getStandardOutput(n, x, nums) << endl;
            fout.close();
    
            cout << "Generated Case " << t << ": n=" << n << ", x=" << x << endl;
        }
    }
    
    int main() {
        make_data();
        cout << "\nAll 10 test cases (1.in~10.out) generated successfully!" << endl;
        return 0;
    }
    

    测试点规划说明 (OJ 建设参考)

    测试点 NN XX 特征描述 考查意图
    1 1 5 最小边界 验证基础逻辑及单元素处理。
    2 5 20 X>TotalSumX > \text{TotalSum} 验证对无解情况(target<0target < 0)的快速判定。
    3 10 1000 X=TotalSumX = \text{TotalSum} 验证对全选情况(target=0target = 0)的处理。
    4 100 10 存在极端大数干扰 考查滑动窗口在数值跨度大时的稳定性。
    5 1000 随机 确保有解构造 验证 O(N)O(N) 逻辑在普通规模下的正确性。
    6-7 10510^5 最大规模随机 压力测试,验证是否会因为使用哈希表或暴力而超时。
    8-10 构造 最大规模确保有解 压力测试,考察指针移动的边界及 long long 的溢出处理。

    教练的生成器编写建议

    1. 构造有解数据的重要性: 单纯的随机数据很难产生“和恰好等于 targettarget”的情况,会导致测试点全是 -1。通过先生成数组,再随机抠掉中间一段来反推 xx,可以确保测试点具有极强的区分度。
    2. 数据类型的选择: 本题中 n=105n=10^5, nums[i]=104nums[i]=10^4,总和会达到 10910^9。虽然 int 勉强能存,但在生成器中使用 mt19937_64long long 可以有效避免生成数据时的溢出风险。
    3. 非递归与性能: 本生成器逻辑完全基于迭代,生成 10 组 10510^5 规模的数据在 1 秒内即可完成,不会出现爆栈或生成超时。
    4. 格式规范: 注意 (i == n - 1 ? "" : " ") 的处理,这是保证输入数据不带多余行末空格的竞赛标准写法。
    • 0
      @ 2025-12-19 23:11:11

      这是“将 x 减到 0 的最小操作数”问题的标准题解。在 NOI 竞赛中,这道题的精髓在于逆向思维转换:将“寻找两端最小”转化为“寻找中间最长”。

      一、 题解标准代码 (C++14)

      #include <iostream>
      #include <vector>
      #include <algorithm>
      
      using namespace std;
      
      /**
       * 核心思路:
       * 设数组总和为 totalSum。
       * 我们要在数组两端移除元素之和为 x,等价于在数组中间找到一段连续子数组,
       * 其和为 target = totalSum - x。
       * 为了让移除的元素最少,我们需要让中间剩下的这段连续子数组长度“最长”。
       */
      
      int main() {
          // 竞赛优化:提升输入输出效率
          ios::sync_with_stdio(false);
          cin.tie(0);
      
          int n;
          long long x; // x 范围可达 10^9,建议使用 long long 
          if (!(cin >> n >> x)) return 0;
      
          vector<int> nums(n);
          long long totalSum = 0;
          for (int i = 0; i < n; ++i) {
              cin >> nums[i];
              totalSum += nums[i]; // 累加总和
          }
      
          // 目标和:中间连续部分应该达到的数值
          long long target = totalSum - x;
      
          // 边界情况 1:如果总和小于 x,无论如何也减不到 0
          if (target < 0) {
              cout << -1 << endl;
              return 0;
          }
      
          // 边界情况 2:如果总和恰好等于 x,说明必须移除所有元素
          if (target == 0) {
              cout << n << endl;
              return 0;
          }
      
          int maxLen = -1;       // 记录中间最长子数组的长度
          long long currentSum = 0;
          int left = 0;          // 滑动窗口左指针
      
          // 滑动窗口遍历
          for (int right = 0; right < n; ++right) {
              currentSum += nums[right]; // 右指针向右扩展,增加窗口和
      
              // 关键点:当窗口和超过 target 时,左指针向右移动缩小窗口
              // 因为所有元素都是正数,所以 currentSum 具有单调性
              while (left <= right && currentSum > target) {
                  currentSum -= nums[left];
                  left++;
              }
      
              // 判定:当前窗口和是否恰好等于目标值
              if (currentSum == target) {
                  maxLen = max(maxLen, right - left + 1);
              }
          }
      
          // 如果 maxLen 仍为 -1,说明没有找到和为 target 的连续子数组
          if (maxLen == -1) {
              cout << -1 << endl;
          } else {
              // 结果 = 总长度 - 中间最长长度
              cout << n - maxLen << endl;
          }
      
          return 0;
      }
      

      二、 复杂度分析思考过程

      1. 时间复杂度分析

      • 输入与求和:遍历一次数组,O(n)O(n)
      • 滑动窗口过程
        • 右指针 right00 遍历到 n1n-1
        • 左指针 left 虽然在嵌套的 while 循环里,但它在整个程序运行过程中只会从左向右移动,不会回退
        • 这意味着数组中的每个元素最多被“加入窗口”一次,被“移出窗口”一次。
      • 结论:总时间复杂度为 O(n)O(n)
        • 对于 n=105n = 10^5,运算量约为 2×1052 \times 10^5 次,远远低于 NOI 1秒限时(通常支持 10810^8 次运算)。

      2. 空间复杂度分析

      • 数组存储:使用了 vector<int> nums 存储 10510^5 个整数,占用内存约为 400400 KB。
      • 辅助变量:仅使用了若干个 long longint 变量。
      • 结论:总空间复杂度为 O(n)O(n),内存消耗极低。

      三、 时间复杂度优化建议

      本题 O(n)O(n) 的线性解法已经是理论上的最优解,但在极端竞赛环境下,仍有细微的优化方向:

      1. 早期停止 (Early Exit)
        • 在求和过程中,如果 totalSum < x,可以直接输出 -1 并退出。
        • 如果 totalSum == x,直接输出 n
      2. 前缀和 + 哈希表 (替代方案)
        • 如果数组中存在负数,滑动窗口将失效(因为窗口和不再具有单调性)。此时必须使用前缀和配合 std::unordered_map
        • 但在本题全是正数的前提下,滑动窗口比哈希表的常数更小,速度更快,且空间复杂度更优(哈希表会有额外的内存开销)。
      3. 读入优化 (Fast I/O)
        • 在 NOI 赛场上,如果 nn 达到 10610^6 或更大,建议使用 getchar() 实现的快读函数,这比 cin 优化后还要快。

      四、 易错点总结(避坑指南)

      • 逆向思维缺失:很多新手会尝试用暴力递归去模拟“切左边还是切右边”,导致 2n2^n 的复杂度而超时。
      • long long 溢出:虽然单个元素 10410^4,但 n=105n=10^5 时的总和可达 10910^9。虽然 int 理论上能承载 2×1092 \times 10^9,但考虑到中间计算,使用 long long 存储 totalSumtarget 是更稳健的竞赛习惯。
      • target 为 0 的判定:当 xx 恰好等于数组总和时,target 为 0。此时滑动窗口需要正确处理空窗口的情况,或者在循环前特判。
      • 无法满足要求:一定要初始化 maxLen = -1,并根据此标志位判断是否输出 -1。如果不加判断,可能会输出 n+1n+1 这种错误结果。
      • 1

      信息

      ID
      19357
      时间
      1000ms
      内存
      128MiB
      难度
      10
      标签
      (无)
      递交数
      1
      已通过
      1
      上传者