2 条题解

  • 0
    @ 2025-12-16 14:24:36

    你好!我是OI金牌教练。

    为了帮助你创建OJ测试数据,我编写了一个全自动的数据生成器。这个程序会一次性生成 1.in/1.out10.in/10.out 共10组测试数据。

    设计思路

    1. 覆盖全面
      • 边界情况N=0N=0(空链表),N=1N=1(单节点),N=300N=300(满数据)。
      • 极端值:包含最小值 -100 和最大值 100
      • 特殊分布:全相等(如 1 1 1)、全不等(如 1 2 3)、只有两个值(如 1...1 2...2)。
    2. 保证有序:题目要求输入是“已排序”的,生成随机数后必须使用 std::sort 排序。
    3. 标准解法集成:为了生成 .out 文件,我在生成器内部内置了一个“绝对正确”的解法(利用 std::vectorstd::unique 特性,等价于链表去重逻辑),确保输出数据的正确性。

    C++ 数据生成器代码 (Data Generator)

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

    /**
     * NOIP/NOI 风格题目数据生成器
     * 题目: LeetCode 83 - 删除排序链表中的重复元素
     * 生成: 1.in/1.out ~ 10.in/10.out
     * 环境: C++14
     */
    
    #include <iostream>
    #include <fstream>
    #include <vector>
    #include <algorithm>
    #include <random>
    #include <string>
    #include <set>
    
    using namespace std;
    
    // 配置参数
    const int MAX_N = 300;     // 最大节点数
    const int MIN_VAL = -100;  // 节点最小值
    const int MAX_VAL = 100;   // 节点最大值
    
    // 随机数生成器初始化
    mt19937 rng(random_device{}());
    
    /**
     * 辅助函数:生成指定范围内的随机整数 [l, r]
     */
    int random_int(int l, int r) {
        uniform_int_distribution<int> dist(l, r);
        return dist(rng);
    }
    
    /**
     * 核心逻辑:解决问题并生成 .out 内容
     * 这里的逻辑等价于链表去重,但为了生成器稳定性,使用 vector 操作
     */
    void solve_and_write(const vector<int>& input_data, string out_filename) {
        ofstream fout(out_filename);
        
        // 复制一份数据以免修改原数据
        vector<int> result = input_data;
        
        // 使用 std::unique 去重,原理和链表去重完全一致:
        // 只有当当前元素 != 前一个元素时保留
        auto last = unique(result.begin(), result.end());
        result.erase(last, result.end());
        
        // 输出结果
        for (size_t i = 0; i < result.size(); ++i) {
            if (i > 0) fout << " ";
            fout << result[i];
        }
        fout << endl;
        
        fout.close();
    }
    
    /**
     * 生成单组测试数据
     * id: 测试点编号
     * n: 节点数量
     * type: 数据类型模式
     */
    void generate_case(int id, int n, int type) {
        string in_file = to_string(id) + ".in";
        string out_file = to_string(id) + ".out";
        
        ofstream fin(in_file);
        
        // 1. 生成输入数据
        vector<int> data;
        
        if (n > 0) {
            if (type == 1) { 
                // 模式1: 全相等 (Worst Case for duplicates)
                int val = random_int(MIN_VAL, MAX_VAL);
                for(int i=0; i<n; ++i) data.push_back(val);
            }
            else if (type == 2) { 
                // 模式2: 全不等 (Best Case, 尽量不重复)
                // 由于范围 -100~100 只有201个数,如果 n > 201 必然重复,这里尽量取不同
                set<int> s;
                while(data.size() < n) {
                    // 如果把范围取完了,只能随机填充
                    if (s.size() >= (MAX_VAL - MIN_VAL + 1)) {
                        data.push_back(random_int(MIN_VAL, MAX_VAL));
                    } else {
                        int val = random_int(MIN_VAL, MAX_VAL);
                        if (s.find(val) == s.end()) {
                            s.insert(val);
                            data.push_back(val);
                        }
                    }
                }
            }
            else if (type == 3) {
                // 模式3: 两极分化 (前一半最小值,后一半最大值)
                for(int i=0; i<n/2; ++i) data.push_back(MIN_VAL);
                for(int i=n/2; i<n; ++i) data.push_back(MAX_VAL);
            }
            else {
                // 模式0: 完全随机 (Random)
                for(int i=0; i<n; ++i) {
                    data.push_back(random_int(MIN_VAL, MAX_VAL));
                }
            }
        }
    
        // 题目关键要求:输入必须是有序的
        sort(data.begin(), data.end());
    
        // 2. 写入 .in 文件
        fin << n << endl;
        for (size_t i = 0; i < data.size(); ++i) {
            if (i > 0) fin << " ";
            fin << data[i];
        }
        // 即便 n=0 也要换行,保持格式整洁
        fin << endl;
        fin.close();
    
        // 3. 写入 .out 文件 (调用解法)
        solve_and_write(data, out_file);
        
        cout << "Generated Test Case " << id << ": N = " << n << endl;
    }
    
    int main() {
        // 优化生成速度
        ios::sync_with_stdio(false);
    
        // --- 构造测试点 ---
        
        // 1. 边界:空链表
        generate_case(1, 0, 0); 
        
        // 2. 边界:单个节点
        generate_case(2, 1, 0);
        
        // 3. 边界:两个节点,值相同
        generate_case(3, 2, 1);
        
        // 4. 边界:两个节点,值不同
        generate_case(4, 2, 2);
        
        // 5. 小数据:全相等
        generate_case(5, 10, 1);
        
        // 6. 小数据:全不等 (密集去重测试)
        generate_case(6, 20, 2);
        
        // 7. 中等数据:随机
        generate_case(7, 50, 0);
        
        // 8. 满数据:只有两个值 (测试大量跳跃)
        generate_case(8, 300, 3);
        
        // 9. 满数据:随机分布
        generate_case(9, 300, 0);
        
        // 10. 满数据:随机分布 (再次验证)
        generate_case(10, 300, 0);
    
        cout << "All 10 test cases generated successfully." << endl;
        return 0;
    }
    

    数据点详细说明 (Coverage Analysis)

    测试点 ID N (节点数) 数据特征 考察点
    1 0 边界处理:程序不应崩溃,输出应为空或仅换行。
    2 1 随机值 边界处理:单个节点不应触发 next->val 检查。
    3 2 值相同 (如 5 5) 基础逻辑:能否删除末尾的重复元素。
    4 值不同 (如 1 2) 基础逻辑:不删除的情况。
    5 10 全相等 (如 3 3 ... 3) 循环删除:能否处理连续的重复,只保留一个。
    6 20 全不等 (如 -10 -9 ...) 遍历逻辑:每个节点都保留,指针移动是否正确。
    7 50 随机 常规情况:混合了重复和不重复。
    8 300 两段式 (如 -100... 100...) 特殊构造:前半段大量删除,后半段大量删除,中间跳跃。
    9 随机 压力测试:最大数据量下的常规表现。
    10 压力测试:验证解的稳定性。

    使用方法

    1. 将代码保存为 gen.cpp
    2. 编译:g++ gen.cpp -o gen -O2 -std=c++14
    3. 运行:./gen(Windows 下是 gen.exe)。
    4. 你的目录下会出现 1.in, 1.out ... 10.out
    5. 将这些文件打包上传到你的 OJ 系统后台即可。
    • 0
      @ 2025-12-16 14:22:20

      你好!作为金牌教练,我直接给出这份符合 NOIP/NOI 竞赛标准 的标准代码(Standard Solution)。

      这份代码采用了 C++14 标准,不仅实现了核心逻辑,还包含了完整的输入输出处理框架。代码中我特意在容易写挂的地方加了详细注释。


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

      /*
       * 题目: 有序链表去重 (Remove Duplicates from Sorted List)
       * 标签: 链表 (Linked List), 模拟 (Simulation)
       * 难度: NOIP 普及组- / 入门
       * 作者: OI金牌教练
       */
      
      #include <iostream>  // 标准输入输出流
      #include <algorithm> // 通用算法库
      #include <vector>
      
      using namespace std;
      
      // --- 结构体定义 ---
      struct ListNode {
          int val;
          ListNode *next;
          
          // 构造函数:初始化值和空指针
          ListNode(int x) : val(x), next(nullptr) {}
      };
      
      // --- 核心算法函数 ---
      ListNode* deleteDuplicates(ListNode* head) {
          // [边界判断] 如果链表为空或只有一个节点,无需处理,直接返回
          if (head == nullptr || head->next == nullptr) {
              return head;
          }
      
          ListNode* cur = head;
      
          // [循环条件] 必须保证当前节点(cur)和下一个节点(cur->next)都存在
          // 否则比较 cur->next->val 时会发生段错误 (Segmentation Fault)
          while (cur->next != nullptr) {
              
              // [核心逻辑]
              if (cur->val == cur->next->val) {
                  // 发现重复:当前值 == 下一个值
                  ListNode* duplicate = cur->next; // 1. 暂存待删除节点
                  cur->next = duplicate->next;     // 2. 修改指针:跳过重复节点
                  delete duplicate;                // 3. 释放内存 (养成好习惯)
                  
                  // [易错点警告]
                  // 删除后,不要移动 cur 指针!
                  // 因为新的 cur->next 可能仍然和 cur->val 相等(例如 1->1->1 的情况)
                  // 下一次循环需要再次检查当前的 cur 和 新的 next
              } else {
                  // 没有重复:才移动指针到下一位
                  cur = cur->next;
              }
          }
      
          return head;
      }
      
      int main() {
          // [IO优化] 关闭stdio同步,加快读写速度,这是竞赛必备习惯
          ios::sync_with_stdio(false);
          cin.tie(nullptr);
      
          int n;
          // 处理可能的读取失败或EOF
          if (!(cin >> n)) return 0;
      
          // 特判 n=0 的情况,虽然题目范围通常有保障,但在竞赛中不仅要看范围,还要防一手空数据
          if (n == 0) {
              return 0;
          }
      
          // --- 建表过程 (尾插法) ---
          ListNode* head = nullptr;
          ListNode* tail = nullptr;
      
          for (int i = 0; i < n; ++i) {
              int val;
              cin >> val;
              // 动态开辟节点
              ListNode* newNode = new ListNode(val);
      
              if (head == nullptr) {
                  // 第一个节点既是头也是尾
                  head = newNode;
                  tail = newNode;
              } else {
                  // 将新节点挂在尾部,并更新尾指针
                  tail->next = newNode;
                  tail = newNode;
              }
          }
      
          // --- 调用核心算法 ---
          head = deleteDuplicates(head);
      
          // --- 输出结果 ---
          ListNode* cur = head;
          bool first = true; // 用于控制空格格式
          while (cur != nullptr) {
              if (!first) cout << " ";
              cout << cur->val;
              first = false;
              cur = cur->next;
          }
          cout << endl; // 结尾换行,符合竞赛输出规范
      
          // 竞赛中通常由操作系统统一回收进程内存,
          // 手动释放整个链表的代码在限时比赛中通常省略。
          
          return 0;
      }
      

      二、 复杂度分析思考过程

      在草稿纸上推演算法时,我们是这样分析复杂度的:

      1. 时间复杂度分析

      • 思考过程
        • 代码主要有一个 while 循环:while (cur->next != nullptr)
        • 在循环体内,我们做的事情是比较 cur->valcur->next->val
        • 如果不相等,cur 向后移动一步。
        • 如果相等,我们删除了一个节点,虽然 cur 没动,但链表的总长度 NN 减少了 1。
        • 这意味着:每个节点要么被 cur 访问并跳过,要么被删除。
        • 任何一个节点都不会被反复处理超过常数次。
      • 结论:时间复杂度为 O(N)O(N),其中 NN 是链表的长度。这是最优复杂度,因为我们必须至少读取一次所有数据。

      2. 空间复杂度分析

      • 思考过程
        • 我们定义了 head, tail, cur, duplicate 等几个指针变量。
        • 这些变量占用的空间是固定的,不会随着数据量 NN 的增大而增大。
        • 注意:虽然链表本身占用了 O(N)O(N) 的空间,但那是存储输入数据必须的。算法的额外空间复杂度(Auxiliary Space Complexity)只看我们算法运行时额外申请的部分。
      • 结论:空间复杂度为 O(1)O(1)(原地去重)。

      三、 时间复杂度优化建议(进阶)

      虽然 O(N)O(N) 已经是理论最优,但在**实际竞赛(如 NOIP/NOI)**中,如果数据量极其巨大(例如 N=106N=10^610710^7),频繁的 newdelete 操作(动态内存分配)会非常慢,甚至导致超时(TLE)。

      优化方案:静态数组模拟链表(Memory Pool)

      这是高阶选手的通用技巧。我们不使用结构体指针,而是用两个数组 val[N]next_idx[N] 来模拟。

      • 优点
        1. 没有动态分配内存的开销,速度极快。
        2. 内存连续,缓存命中率高(Cache Friendly)。
      • 代码片段示意
      const int MAXN = 300005;
      int val[MAXN];      // 存储节点的值
      int nxt[MAXN];      // 存储下一个节点的下标 (代替指针)
      int head = 1;       // 头结点下标
      int idx = 0;        // 内存池计数器
      
      // 核心逻辑变化
      int curr = head;
      while (nxt[curr] != 0) { // 0 表示 nullptr
          if (val[curr] == val[nxt[curr]]) {
              nxt[curr] = nxt[nxt[curr]]; // 直接修改下标指向,跳过
          } else {
              curr = nxt[curr];
          }
      }
      

      总结:对于本题 N300N \le 300 的范围,使用标准的 struct 指针写法更清晰、更符合题目本意。但如果遇到 N106N \ge 10^6,请务必切换到静态数组模拟链表

      • 1

      信息

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