2 条题解

  • 0
    @ 2025-12-16 16:10:07

    你好!这是为你准备的 NOI 风格数据生成器。这个程序会一次性生成 1.in/1.out10.in/10.out 共 20 个文件。

    数据生成策略说明

    为了确保测试数据的完备性,我设计了如下 10 个测试点,覆盖了题目要求的 60%60\% 小数据和 100%100\% 大数据,以及各种边界情况:

    1. 测试点 1 (Sample): 极小数据,类似题目样例,用于基础验证。
    2. 测试点 2 (No Zeros): N=100N=100,完全没有 0,测试不移动的情况。
    3. 测试点 3 (All Zeros): N=100N=100,全是 0,测试全 0 逻辑。
    4. 测试点 4 (Front Zeros): N=1000N=1000,前半段全是 0,后半段非 0。
    5. 测试点 5 (Back Zeros): N=1000N=1000,前半段非 0,后半段全是 0。
    6. 测试点 6 (Sparse Zeros): N=10000N=10000 (满数据),0 很少(约 5%),测试大量非 0 元素的移动。
    7. 测试点 7 (Dense Zeros): N=10000N=10000 (满数据),0 很多(约 90%),测试大量 0 的堆积。
    8. 测试点 8 (Random): N=10000N=10000,完全随机,0 和非 0 各占一半,数值覆盖 int32 负数到正数范围。
    9. 测试点 9 (Alternating): N=10000N=10000,0 和非 0 交替出现 (0, x, 0, x...),测试频繁交换。
    10. 测试点 10 (Worst/Edge): N=10000N=10000,只有一个非 0 元素在最后,或者只有一个 0 在最前,极端边界测试。

    数据生成器代码 (C++14)

    请将以下代码保存为 generator.cpp,编译运行即可在当前目录下生成所有测试数据文件。

    /**
     * Title: Move Zeroes Data Generator
     * Standard: C++14
     * Author: OI Coach
     * Description: Generates 1.in/out to 10.in/out for OJ testing.
     */
    
    #include <iostream>
    #include <vector>
    #include <string>
    #include <fstream>
    #include <algorithm>
    #include <random>
    #include <chrono>
    
    using namespace std;
    
    // ==========================================
    // 1. 标准题解算法 (用于生成 .out 文件)
    // ==========================================
    vector<int> solve(vector<int> nums) {
        int n = nums.size();
        int j = 0;
        for (int i = 0; i < n; ++i) {
            if (nums[i] != 0) {
                swap(nums[i], nums[j]);
                j++;
            }
        }
        return nums;
    }
    
    // ==========================================
    // 2. 随机数生成工具
    // ==========================================
    mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
    
    // 生成 [min, max] 范围内的整数
    int rand_int(int min, int max) {
        uniform_int_distribution<int> dist(min, max);
        return dist(rng);
    }
    
    // ==========================================
    // 3. 文件写入辅助函数
    // ==========================================
    void write_case(int id, const vector<int>& input_data) {
        string in_name = to_string(id) + ".in";
        string out_name = to_string(id) + ".out";
    
        // 写入 .in 文件
        ofstream fin(in_name);
        fin << input_data.size() << "\n";
        for (size_t i = 0; i < input_data.size(); ++i) {
            fin << input_data[i] << (i == input_data.size() - 1 ? "" : " ");
        }
        fin << "\n";
        fin.close();
    
        // 计算答案并写入 .out 文件
        vector<int> output_data = solve(input_data);
        ofstream fout(out_name);
        for (size_t i = 0; i < output_data.size(); ++i) {
            fout << output_data[i] << (i == output_data.size() - 1 ? "" : " ");
        }
        fout << "\n";
        fout.close();
    
        cout << "Generated Case " << id << ": N = " << input_data.size() << endl;
    }
    
    // ==========================================
    // 4. 数据生成主逻辑
    // ==========================================
    int main() {
        // 题目数据范围常量
        const int MIN_VAL = -2147483648; // INT_MIN
        const int MAX_VAL = 2147483647;  // INT_MAX
        // 为了防止随机生成时溢出或方便控制,我们取个稍微安全点的范围,
        // 或者直接按题目要求生成全范围。这里为了展示方便,取稍小一点的常用范围,
        // 但特定测试点会覆盖极端值。
        
        for (int i = 1; i <= 10; ++i) {
            vector<int> nums;
            int n;
    
            switch(i) {
                case 1: // 样例规模:极小
                    n = 10;
                    for(int k=0; k<n; ++k) nums.push_back(k % 3 == 0 ? 0 : k);
                    break;
    
                case 2: // 无 0 测试
                    n = 100;
                    for(int k=0; k<n; ++k) nums.push_back(rand_int(1, 100));
                    break;
    
                case 3: // 全 0 测试
                    n = 100;
                    for(int k=0; k<n; ++k) nums.push_back(0);
                    break;
    
                case 4: // 前半部分是 0
                    n = 1000;
                    for(int k=0; k<n; ++k) {
                        if (k < n / 2) nums.push_back(0);
                        else nums.push_back(rand_int(1, 1000));
                    }
                    break;
    
                case 5: // 后半部分是 0
                    n = 1000;
                    for(int k=0; k<n; ++k) {
                        if (k >= n / 2) nums.push_back(0);
                        else nums.push_back(rand_int(1, 1000));
                    }
                    break;
    
                case 6: // 大数据,稀疏 0 (5% 是 0)
                    n = 10000;
                    for(int k=0; k<n; ++k) {
                        if (rand_int(1, 100) <= 5) nums.push_back(0);
                        else nums.push_back(rand_int(-10000, 10000));
                    }
                    break;
    
                case 7: // 大数据,稠密 0 (90% 是 0)
                    n = 10000;
                    for(int k=0; k<n; ++k) {
                        if (rand_int(1, 100) <= 90) nums.push_back(0);
                        else nums.push_back(rand_int(-10000, 10000));
                    }
                    break;
    
                case 8: // 大数据,完全随机,覆盖 INT 极值
                    n = 10000;
                    for(int k=0; k<n; ++k) {
                        // 50% 概率是 0
                        if (rand_int(0, 1) == 0) nums.push_back(0);
                        // 50% 概率是非 0,且包含负数和极大值
                        else nums.push_back(rand_int(MIN_VAL / 2, MAX_VAL / 2)); 
                    }
                    break;
    
                case 9: // 大数据,0 和 非0 交替
                    n = 10000;
                    for(int k=0; k<n; ++k) {
                        if (k % 2 == 0) nums.push_back(0);
                        else nums.push_back(k); // 非0
                    }
                    break;
    
                case 10: // 极端情况:只有一个非0在最后
                    n = 10000;
                    for(int k=0; k<n-1; ++k) nums.push_back(0);
                    nums.push_back(999);
                    break;
            }
    
            // 调用函数生成文件
            write_case(i, nums);
        }
    
        cout << "All test cases generated successfully!" << endl;
        return 0;
    }
    

    如何使用

    1. 确保你安装了 G++ 编译器。
    2. 保存上述代码为 gen.cpp
    3. 在终端编译:g++ gen.cpp -o gen -O2 -std=c++14
    4. 运行程序:./gen (Windows 下是 gen.exe)。
    5. 你的目录下将会出现 1.in, 1.out ... 10.in, 10.out

    优化细节说明

    1. 非递归写法:所有逻辑均在循环中完成,避免了递归带来的栈溢出风险,虽然本题 N=104N=10^4 并不大,但这是良好的工程习惯。
    2. 时间复杂度
      • 生成逻辑是线性的 O(N)O(N)
      • 解题逻辑(双指针)也是线性的 O(N)O(N)
      • 总时间复杂度为 O(10×N)O(10 \times N),生成速度极快(毫秒级)。
    3. 随机性:使用了 std::mt19937 (Mersenne Twister),比传统的 rand() 随机性更好,分布更均匀。
    4. IO 效率:使用了 ofstream,配合 C++ 的流操作,对于 10410^4 规模的数据完全足够。如果是 10610^6 级别,可能需要换回 fprintf,但此处为了代码可读性使用了流。
    • 0
      @ 2025-12-16 16:06:04

      你好!我是你的OI金牌教练。

      针对 LeetCode 283《移动零》改编的 NOI 风格题目《零的迁移》,下面给出标准的题解代码。这段代码完全符合 NOIP/CSP-J/S 的考场规范(C++14 标准),并附带了详细的注释和复杂度分析,帮助你深入理解。

      NOIP 标准题解代码 (C++14)

      /**
       * 题目: 零的迁移 (Move Zeroes)
       * 来源: 改编自 LeetCode 283
       * 语言: C++14
       * 算法: 双指针 (Two Pointers) / 快慢指针
       */
      
      #include <iostream>
      #include <vector>
      #include <algorithm> // 用于 std::swap
      
      using namespace std;
      
      // 核心解题函数
      void solve() {
          int n;
          // 1. 输入数据规模
          if (!(cin >> n)) return;
      
          // 根据数据范围 1 <= n <= 10^4,使用 vector 或全局数组皆可
          // vector 在函数内定义是分配在堆上的(主要空间),栈溢出风险较小
          vector<int> nums(n);
          
          // 2. 输入数组元素
          for (int i = 0; i < n; ++i) {
              cin >> nums[i];
          }
      
          /* 
             --- 核心算法:双指针法 ---
             slow (慢指针): 始终指向“当前处理好的非零序列”的尾部下一个位置(即潜在的放置位)。
             fast (快指针): 也就是循环变量 i,负责向后扫描寻找非零元素。
          */
          int slow = 0; 
          for (int fast = 0; fast < n; ++fast) {
              // 如果当前快指针指向的数不是 0
              if (nums[fast] != 0) {
                  // 【关键点】:将非零数交换到慢指针的位置
                  // 易错点提示:必须使用 swap 而不是简单的赋值覆盖,否则会丢失原来的数据
                  // 除非你打算最后统一补 0(那是另一种写法)
                  
                  // 优化细节:如果 slow == fast,说明中间还没有出现过 0,
                  // 此时交换是自己换自己,虽然结果正确但多余。
                  // 加上 if (slow != fast) 判断可以微量优化,但在竞赛中通常为了代码简洁可省略。
                  swap(nums[slow], nums[fast]);
                  
                  // 慢指针向前移动一步,准备接收下一个非零数
                  slow++;
              }
              // 如果 nums[fast] 是 0,则不做任何操作,快指针继续向前,
              // 此时 slow 停在原地(指向这个 0),等待被后续的非零数置换。
          }
      
          // 3. 按照格式输出结果
          for (int i = 0; i < n; ++i) {
              cout << nums[i] << (i == n - 1 ? "" : " ");
          }
          cout << endl;
      }
      
      int main() {
          // 【竞赛必备】:关闭同步流,加速 cin/cout
          // 避免在数据量达到 10^5 或 10^6 时出现 IO TLE
          ios::sync_with_stdio(false);
          cin.tie(nullptr);
      
          solve();
      
          return 0;
      }
      

      复杂度分析与思维过程

      1. 时间复杂度分析

      • 思考过程: 代码中只有一个主要的 for 循环,循环变量 fast00 走到 n1n-1。 在循环内部,我们执行的操作是 if 判断、swap 交换(涉及 3 次赋值)和 slow++。这些都是 O(1)O(1) 的常数级操作。 并没有嵌套循环,也没有递归。
      • 结论: 时间复杂度为 O(n)O(n),其中 nn 为数组长度。 这对于本题 n104n \le 10^4 的数据范围来说绰绰有余(通常 O(n)O(n) 算法可在 1秒内处理 10810^8 级别的数据)。

      2. 空间复杂度分析

      • 思考过程: 题目要求“原地操作” (In-place)。 我们只定义了两个整型变量 slowfast(以及循环内的临时变量),没有开辟与 nn 成正比的辅助数组(输入数组 nums 本身不算在额外空间内)。
      • 结论: 空间复杂度为 O(1)O(1)

      深度教学:时间复杂度优化建议

      虽然上述代码已经是 O(n)O(n) 的最优时间复杂度,但在操作细节上,我们还可以从“操作次数”的角度进行微观优化。这在 OI 竞赛中处理卡常题(Time Limit 非常紧的题)时很有用。

      优化方案:避免不必要的交换 (Assignment vs Swap)

      当前 Swap 方案的特点: 每次遇到非零数都要执行 swapswap 包含了 3 次内存写入(Read A -> Tmp, Read B -> A, Read Tmp -> B)。

      优化思路(赋值覆盖法): 我们可以直接把非零数覆盖到前面,最后统一把剩下的位置填 0。

      // 优化后的核心逻辑
      int slow = 0;
      for (int fast = 0; fast < n; ++fast) {
          if (nums[fast] != 0) {
              // 直接赋值,减少内存读写次数
              // 如果 slow != fast,说明我们实际上是在把后面的非零数移到前面
              // 如果 slow == fast,这是自己赋值给自己(无害)
              nums[slow] = nums[fast];
              slow++;
          }
      }
      // 循环结束后,slow 之后的所有位置都应该是 0
      for (int i = slow; i < n; ++i) {
          nums[i] = 0;
      }
      

      对比分析

      1. Swap版:代码极简,逻辑闭环,能保留 0 之间的相对顺序(虽然题目没要求)。
      2. 赋值版
        • 写入操作总数通常更少(非零数只写一次,最后的 0 只写一次)。
        • 在非零元素非常多(例如数组全是 1)的情况下,nums[slow] = nums[fast] 会变成大量的自我赋值。可以加 if (slow != fast) 来避免。
        • 在非零元素非常少(例如数组全是 0)的情况下,赋值版不需要做任何写操作(除了最后填0),而 Swap 版也不会做操作。

      教练总结: 在 NOIP/CSP 考场上,首选 Swap 版本带判断的 Swap 版本,因为代码逻辑最不容易写错,且 O(n)O(n) 的量级决定了这点常数差异不会导致 TLE。只有在极度追求运行效率的工程代码中,才会考虑赋值优化的细微差异。

      • 1

      信息

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