2 条题解

  • 0
    @ 2025-12-16 15:55:19

    这是一个非常专业的需求。在搭建OJ(Online Judge)或进行比赛命题时,一套覆盖全面、具有强壮性的测试数据是至关重要的。

    由于本题数据范围较小 (N100N \le 100),我们不需要担心数据生成的时间复杂度,重点在于测试用例的覆盖面(边界值、空值、全删、全保留等)。

    以下是完整的 C++ 数据生成器代码。该程序运行后,会在当前目录下生成 1.in/1.out10.in/10.out 共20个文件。

    数据生成器代码 (Data Generator)

    /**
     * 题目:数组元素的就地移除 - 数据生成器
     * 用途:生成测试点 1.in/out ~ 10.in/out
     * 标准:C++14
     * 策略:覆盖空数组、单元素、全删除、全保留、部分删除、最大数据范围等情况
     */
    
    #include <iostream>
    #include <fstream>
    #include <vector>
    #include <string>
    #include <random>
    #include <algorithm>
    #include <chrono>
    
    using namespace std;
    
    // 全局随机数引擎
    mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
    
    // 生成指定范围内的随机整数 [min, max]
    int randomInt(int min, int max) {
        uniform_int_distribution<int> dist(min, max);
        return dist(rng);
    }
    
    // 核心解题逻辑:用于生成 .out 文件
    // 返回 pair<新长度, 新数组>
    pair<int, vector<int>> solve(vector<int> nums, int val) {
        int k = 0;
        int n = nums.size();
        // 模拟题目要求的“原地移除”逻辑,这里为了方便输出,我们直接在原副本上操作
        for (int i = 0; i < n; ++i) {
            if (nums[i] != val) {
                nums[k] = nums[i];
                k++;
            }
        }
        // 截断数组,只保留前k个有效元素
        nums.resize(k);
        return {k, nums};
    }
    
    // 保存测试点文件
    void saveTestCase(int caseNum, int n, const vector<int>& nums, int val) {
        string inName = to_string(caseNum) + ".in";
        string outName = to_string(caseNum) + ".out";
    
        // 1. 写入 .in 文件
        ofstream fin(inName);
        fin << n << "\n";
        for (int i = 0; i < n; ++i) {
            fin << nums[i] << (i == n - 1 ? "" : " ");
        }
        fin << "\n" << val << "\n";
        fin.close();
    
        // 2. 计算答案并写入 .out 文件
        pair<int, vector<int>> result = solve(nums, val);
        int k = result.first;
        vector<int> resNums = result.second;
    
        ofstream fout(outName);
        fout << k << "\n";
        for (int i = 0; i < k; ++i) {
            fout << resNums[i] << (i == k - 1 ? "" : " ");
        }
        fout << "\n";
        fout.close();
    
        cout << "Generated Case " << caseNum << ": N=" << n << ", Val=" << val 
             << ", Removed=" << (n - k) << endl;
    }
    
    int main() {
        // 【测试点 1-2】:小规模随机数据 (N <= 10)
        for (int i = 1; i <= 2; ++i) {
            int n = randomInt(5, 10);
            vector<int> nums(n);
            for(int& x : nums) x = randomInt(0, 10);
            int val = randomInt(0, 5); // 较高概率命中
            saveTestCase(i, n, nums, val);
        }
    
        // 【测试点 3】:边界情况 - 空数组 (N = 0)
        {
            saveTestCase(3, 0, {}, 5);
        }
    
        // 【测试点 4】:边界情况 - 单元素数组,且需要移除 (N = 1, match)
        {
            saveTestCase(4, 1, {10}, 10);
        }
    
        // 【测试点 5】:边界情况 - 单元素数组,不需要移除 (N = 1, no match)
        {
            saveTestCase(5, 1, {10}, 20);
        }
    
        // 【测试点 6】:特殊情况 - 全部元素都需要移除 (All match)
        {
            int n = 100;
            int val = 7;
            vector<int> nums(n, val); // 数组全为7
            saveTestCase(6, n, nums, val);
        }
    
        // 【测试点 7】:特殊情况 - 没有元素需要移除 (No match)
        {
            int n = 100;
            int val = 50;
            vector<int> nums(n);
            for(int& x : nums) x = randomInt(0, 49); // 保证不等于50
            saveTestCase(7, n, nums, val);
        }
    
        // 【测试点 8】:特殊情况 - 元素值只有两种 (Dense binary case)
        // 模拟类似 [0,1,0,1,1,0] 的情况,测试密集删除
        {
            int n = 100;
            int val = 1;
            vector<int> nums(n);
            for(int& x : nums) x = randomInt(0, 1); // 只有0或1
            saveTestCase(8, n, nums, val);
        }
    
        // 【测试点 9】:中等规模随机 - val 取值在范围外 (Boundary Val)
        // 题目数据范围 nums[i] <= 50, val <= 100。测试 val > 50 的情况
        {
            int n = 80;
            int val = 99; // 数组里最大只有50,所以肯定删不掉任何东西
            vector<int> nums(n);
            for(int& x : nums) x = randomInt(0, 50);
            saveTestCase(9, n, nums, val);
        }
    
        // 【测试点 10】:大规模随机数据 (N = 100)
        // 综合强度测试
        {
            int n = 100;
            int val = randomInt(0, 50);
            vector<int> nums(n);
            // 让 val 出现的频率大约在 1/10 左右,稍微稀疏一点
            for(int& x : nums) {
                if (randomInt(0, 9) == 0) x = val;
                else x = randomInt(0, 50);
            }
            saveTestCase(10, n, nums, val);
        }
    
        cout << "All 10 test cases generated successfully!" << endl;
        return 0;
    }
    

    数据覆盖点说明表

    为了确保你的测试数据具有说服力,你可以参考下表来检查覆盖情况:

    测试点编号 NN 的大小 数据特征 考察目的 预期结果
    1-2 小 (5105-10) 纯随机 基础功能验证 正常移除
    3 00 空数组 边界健壮性:程序是否会崩溃或越界 k=0k=0
    4 11 唯一元素 == val 边界健壮性:删成空数组
    5 唯一元素 != val 边界健壮性:保留唯一元素 k=1k=1
    6 100100 (Max) 所有元素都等于 val 极端情况:能否处理全部删除 k=0k=0
    7 没有元素等于 val 极端情况:能否处理全部保留 k=100k=100
    8 只有 0 和 1 密集数据:频繁的移除操作 k50k \approx 50
    9 8080 val 不在数组值域内 逻辑检查val 很大时的处理 k=Nk=N
    10 100100 (Max) 大规模随机 综合性能测试 随机 kk

    核心知识点与生成器设计思路

    1. 文件流操作 (ofstream):生成器需要频繁创建文件,使用 to_string(i) + ".in" 动态生成文件名非常方便。
    2. 随机数分布 (uniform_int_distribution):C++11 的 <random> 库比传统的 rand() 更均匀,更适合生成竞赛数据,避免伪随机数的周期性问题。
    3. 解耦设计:将“解题逻辑”封装在 solve 函数中。这不仅让代码清晰,而且保证了生成的 .out 文件绝对是基于你的逻辑算出来的,所见即所得
    4. 边界覆盖
      • N=0N=0 往往是许多初学者代码 Segmentation Fault 的原因(例如直接访问 nums[0] 而不检查长度)。
      • 全删(结果为空)容易导致输出格式错误(例如输出了多余的空格或换行)。

    将这段代码保存为 gen.cpp,编译运行即可在当前文件夹下得到全套测试数据。

    • 0
      @ 2025-12-16 15:42:33

      你好!我是你的OI竞赛教练。

      针对题目《数组元素的就地移除》,这是在NOI/CSP普及组或提高组初赛中考察线性表操作双指针算法的经典模型。

      下面我将给出标准的 AC (Accepted) 代码,采用 NOIP C++14 标准编写,并附带详细的复杂度分析与优化思路。

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

      这份代码可以直接在本地编译运行,也可以用于提交评测。我在关键逻辑处加入了详细注释,特别是新手容易写错的边界条件

      /*
       * 题目:数组元素的就地移除 (Remove Element In-Place)
       * 算法:双指针法 (快慢指针)
       * 时间复杂度:O(N)
       * 空间复杂度:O(1)
       * 编写标准:NOIP/CSP C++14
       */
      
      #include <iostream>
      #include <vector>
      
      using namespace std;
      
      int main() {
          // 【关键点1】关闭流同步,加速cin/cout,防止在大数据量下超时
          // 这是NOI系列竞赛的必备习惯
          ios::sync_with_stdio(false);
          cin.tie(nullptr);
      
          int n;
          // 检查输入流状态,虽然题目保证输入合法,但这是一个好习惯
          if (!(cin >> n)) return 0;
      
          // 定义数组
          // 这里使用vector动态调整大小,也可以根据数据范围(N<=100)开静态数组 int nums[105];
          vector<int> nums(n);
          for (int i = 0; i < n; ++i) {
              cin >> nums[i];
          }
      
          int val;
          cin >> val;
      
          // 【核心算法】双指针 - 快慢指针法
          // k (慢指针): 指向当前“新数组”的末尾,即下一个“非val元素”应该存放的位置
          // i (快指针): 负责遍历原数组,寻找“非val元素”
          int k = 0; 
      
          for (int i = 0; i < n; ++i) {
              // 【易错点1】判断条件是 != val,我们保留的是“不等于”val的数
              if (nums[i] != val) {
                  // 将找到的好数 nums[i] 复制到 nums[k] 的位置
                  nums[k] = nums[i];
                  
                  // 【易错点2】赋值后,慢指针才向后移动
                  // 如果写成 nums[k++] = nums[i]; 也是可以的,更简洁
                  k++;
              }
              // 如果 nums[i] == val,快指针 i 继续跑,慢指针 k 原地不动,
              // 相当于把这个坏数“逻辑上”跳过了(实际上稍后会被后面的好数覆盖)。
          }
      
          // 【输出处理】
          // 第一行输出新长度
          cout << k << endl;
      
          // 第二行输出新数组的内容
          // 【易错点3】注意遍历范围是 0 到 k-1,不要输出原数组长度 n
          for (int i = 0; i < k; ++i) {
              // 控制空格:最后一个元素后面通常不加空格,虽然本题可能不严格,但这是竞赛标准格式
              cout << nums[i] << (i == k - 1 ? "" : " ");
          }
          cout << endl; // 保持输出完整性
      
          return 0;
      }
      

      二、 复杂度分析思考过程

      在竞赛中,写完代码后的第一件事就是分析复杂度,确保不会 TLE (Time Limit Exceeded) 或 MLE (Memory Limit Exceeded)。

      1. 时间复杂度分析

      • 思考过程: 我们看代码中的主循环 for (int i = 0; i < n; ++i)
      • 推导: 快指针 i0 走到 n-1,数组中的每个元素被访问了一次。循环内部的操作(比较、赋值、自增)都是 O(1)O(1) 的常数级操作。
      • 结论: 总时间复杂度为 O(N)O(N)。其中 NN 是数组的长度。
        • 对比: 如果使用暴力法(发现一个删一个,把后面的所有元素往前挪),删除一个元素需要移动后面所有元素,最坏情况是 O(N)O(N),总共有 NN 个元素,总复杂度会退化为 O(N2)O(N^2)。在 N=105N=10^5 时暴力法会超时,而双指针法依然能在 0.1秒内完成。

      2. 空间复杂度分析

      • 思考过程: 我们申请了什么新的内存空间?
      • 推导: 除了存储输入数据的数组 nums(这是必须的)之外,我们只定义了两个整数变量 ik 以及 n, val
      • 结论: 额外使用的空间是常数级别的,即 O(1)O(1)。符合题目“原地 (In-Place)”的要求。

      三、 优化建议 (进阶思考)

      虽然 O(N)O(N) 已经是该问题的最优时间复杂度量级,但在特定数据分布下,我们还可以通过减少赋值次数来进行常数级优化。

      场景假设: 数组很大(例如 N=1000N=1000),但要移除的元素 val 非常少(例如只有 1 个),且题目允许打乱元素顺序

      当前解法不足: 如果数组是 [1, 2, 3, 4, 5]val = 1。 快慢指针法会将 2, 3, 4, 5 全部向前移动一位。操作了 4 次赋值。

      优化思路:对撞指针(交换移除) 当我们遇到一个 val 时,不需要把后面的所有人都往前挪,我们可以直接把数组最后一个元素搬过来,覆盖掉当前的 val,然后数组长度减 1。

      优化代码片段:

      // 优化版核心逻辑(注意:这会改变元素相对顺序)
      int left = 0;
      int right = n; // 有效长度
      
      while (left < right) {
          if (nums[left] == val) {
              // 发现坏数,直接用当前有效数组的最后一个元素覆盖它
              nums[left] = nums[right - 1];
              // 数组有效长度减小
              right--;
              // 注意:这里 left 不自增,因为新换过来的数(nums[right-1])可能也是 val,
              // 下一轮循环需要再次检查 nums[left]
          } else {
              left++;
          }
      }
      return right;
      

      对比总结:

      • 快慢指针(标准解法): 保持元素相对顺序,赋值次数等于“好元素”的数量。
      • 对撞指针(优化解法): 保持相对顺序,赋值次数等于“坏元素”的数量。
      • 竞赛建议: 除非题目明确要求“最少操作次数”或数据极度特殊,否则优先使用快慢指针。因为它逻辑更顺畅,不易写出死循环,且“保持相对顺序”在大多数后续题目中是一个默认的有利条件。
      • 1

      信息

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