2 条题解

  • 0
    @ 2025-12-12 10:24:44

    这是一个完整的 C++ 数据生成器,专门为“质粒切割计划”这道题设计。

    它会自动生成 1.in ~ 10.in 和对应的 1.out ~ 10.out。测试数据覆盖了样例、小规模、乱序、已排序、逆序、最大跨度在边界、聚集分布以及 10510^5 规模的大数据压力测试。

    数据生成器代码 (Generator.cpp)

    你需要将此代码保存为 generator.cpp,编译并运行即可。

    /**
     * 题目:质粒切割计划 (Plasmid Cutting)
     * 功能:自动生成 1.in ~ 10.in 及对应的标准答案 .out 文件
     * 覆盖策略:
     * 1. 样例数据
     * 2. 最小边界 (N=2)
     * 3. 基础随机
     * 4. 输入已有序 (升序)
     * 5. 输入已逆序 (降序)
     * 6. 最大片段跨越零点 (测试环形逻辑)
     * 7. 聚集分布 (所有切点集中在一小块,测试大空档)
     * 8. 大数据随机 (N=10^5)
     * 9. 大数据 (N=10^5) 且 L 极大
     * 10. 极限数据
     */
    
    #include <iostream>
    #include <fstream>
    #include <vector>
    #include <string>
    #include <algorithm>
    #include <random>
    #include <ctime>
    #include <set>
    
    using namespace std;
    
    // --- 全局随机数引擎 ---
    mt19937 rng(time(0));
    
    // 辅助函数:生成范围 [min, max] 内的随机整数
    int rand_int(int min_val, int max_val) {
        uniform_int_distribution<int> dist(min_val, max_val);
        return dist(rng);
    }
    
    // --- 核心解题算法 (用于生成 .out) ---
    int solve(int L, int N, vector<int> cuts) {
        // 1. 排序
        sort(cuts.begin(), cuts.end());
    
        int max_len = 0;
    
        // 2. 计算中间段
        for (int i = 1; i < N; ++i) {
            int seg = cuts[i] - cuts[i-1];
            if (seg > max_len) max_len = seg;
        }
    
        // 3. 计算首尾相连段 (关键逻辑)
        int tail_to_head = (L - cuts[N-1]) + cuts[0];
        if (tail_to_head > max_len) max_len = tail_to_head;
    
        return max_len;
    }
    
    // --- 生成唯一切点坐标 ---
    // range_end 是坐标上限 (不包含),即 [0, range_end)
    vector<int> generate_unique_cuts(int n, int range_end, int offset = 0) {
        set<int> s;
        while (s.size() < n) {
            // 保证坐标 < range_end
            int val = rand_int(0, range_end - 1);
            s.insert(val + offset);
        }
        // 转为 vector
        return vector<int>(s.begin(), s.end());
    }
    
    // --- 主生成逻辑 ---
    void make_case(int id) {
        int L, N;
        vector<int> cuts;
    
        // 根据编号定制数据特征
        switch(id) {
            case 1: 
                // 【样例数据】
                L = 100; N = 3;
                cuts = {80, 10, 35};
                break;
    
            case 2:
                // 【最小数据】N=2,测试基础逻辑
                L = 50; N = 2;
                cuts = generate_unique_cuts(N, L);
                break;
    
            case 3:
                // 【小规模随机】N=10,普通乱序
                L = 1000; N = 10;
                cuts = generate_unique_cuts(N, L);
                // 默认 set 生成的是有序的,打乱它以模拟真实乱序输入
                shuffle(cuts.begin(), cuts.end(), rng);
                break;
    
            case 4:
                // 【输入已有序】测试是否会因为已经是升序而出错(不应出错)
                L = 10000; N = 50;
                cuts = generate_unique_cuts(N, L);
                // 此时 cuts 已经是升序,无需 shuffle
                break;
    
            case 5:
                // 【输入为逆序】测试完全降序输入
                L = 10000; N = 50;
                cuts = generate_unique_cuts(N, L);
                sort(cuts.rbegin(), cuts.rend()); // 逆序排列
                break;
    
            case 6:
                // 【最大跨度在边界】
                // 特意让中间都很紧凑,把最大空档留在首尾连接处
                L = 10000; N = 10;
                // 在 [1000, 2000] 之间生成所有点,这样首尾差距巨大
                cuts = generate_unique_cuts(N, 1000, 1000); 
                shuffle(cuts.begin(), cuts.end(), rng);
                break;
    
            case 7:
                // 【聚集分布】
                // 切点全部集中在质粒的前 10% 区域,测试长距离空白
                L = 100000; N = 100;
                cuts = generate_unique_cuts(N, L / 10);
                shuffle(cuts.begin(), cuts.end(), rng);
                break;
    
            case 8:
                // 【大数据压力 1】普通随机
                // N = 10^5, L = 10^8
                L = 100000000; N = 100000;
                cuts = generate_unique_cuts(N, L);
                shuffle(cuts.begin(), cuts.end(), rng);
                break;
    
            case 9:
                // 【大数据压力 2】极端稀疏
                // N = 50000, L = 10^9 (最大范围)
                L = 1000000000; N = 50000;
                cuts = generate_unique_cuts(N, L);
                shuffle(cuts.begin(), cuts.end(), rng);
                break;
    
            case 10:
                // 【极限数据】
                // N = 100,000 (最大值), L = 10^9
                L = 1000000000; N = 100000;
                cuts = generate_unique_cuts(N, L);
                shuffle(cuts.begin(), cuts.end(), rng);
                break;
                
            default:
                L = 100; N = 5; cuts = generate_unique_cuts(N, L); break;
        }
    
        // --- 写入 .in 文件 ---
        string in_filename = to_string(id) + ".in";
        ofstream fout_in(in_filename);
        fout_in << L << " " << N << "\n";
        // 输出 cuts,用空格分隔
        for (size_t i = 0; i < cuts.size(); ++i) {
            fout_in << cuts[i] << (i == cuts.size() - 1 ? "" : " ");
        }
        fout_in << "\n";
        fout_in.close();
    
        // --- 计算并写入 .out 文件 ---
        // 注意:传给 solve 的 cuts 是按值传递的副本,不会影响写入 .in 的原始顺序
        int ans = solve(L, N, cuts); 
        
        string out_filename = to_string(id) + ".out";
        ofstream fout_out(out_filename);
        fout_out << ans << "\n";
        fout_out.close();
    
        cout << "Generated Case " << id << ": L=" << L << ", N=" << N << " -> Ans=" << ans << endl;
    }
    
    int main() {
        cout << "Start generating data for Plasmid Cutting..." << endl;
        for (int i = 1; i <= 10; ++i) {
            make_case(i);
        }
        cout << "All 10 test cases generated successfully!" << endl;
        return 0;
    }
    

    详细说明

    1. 输入乱序模拟

      • std::set 生成的随机数默认是有序的。为了模拟真实考试中“坐标不一定有序”的情况,我在大多数测试点(Case 3, 6, 7, 8, 9, 10)使用了 shuffle 函数将数组打乱。
      • Case 4 保留了升序,Case 5 制造了降序,用来测试选手的排序算法在这些特定情况下的稳定性(尽管 std::sort 通常都没问题,但这是好的测试习惯)。
    2. 边界情况处理 (Case 6)

      • 这是本题的逻辑核心(首尾相连)。
      • 在这个测试点中,我将 NN 个切点全部限制在 [1000,2000][1000, 2000] 范围内,而总长度 L=10000L=10000。这意味着 [2000100001000][2000 \dots 10000 \dots 1000] 这段跨越零点的区域将是最大的片段,强制程序必须正确处理 (L - last) + first 才能得到正确答案。
    3. 大数据生成效率

      • 对于 N=105N=10^5 的情况,使用 std::set 判重生成 10万个随机数可能需要几十毫秒,这是完全可接受的。
      • LL 设为 10910^9(10亿),确保坐标值分布足够稀疏,且不会发生整数溢出(int 最大 21亿)。
    4. 使用步骤

      • 将代码保存为 gen.cpp
      • 编译:g++ gen.cpp -o gen -O2
      • 运行:./gen
      • 你会看到目录下多了 20 个文件,可以直接导入 OJ 系统。
    • 0
      @ 2025-12-12 10:23:21

      这是一个经典的 “环形问题线性化” 题目。核心在于处理环的首尾连接处。

      以下是符合 NOIP/GESP 要求的 C++14 标准题解代码。

      核心代码 (C++14 Standard)

      /**
       * 题目:质粒切割计划 (Plasmid Cutting)
       * 算法:排序 + 线性扫描 (Sorting + Linear Scan)
       * 难度:GESP 5级 / CSP-J T2
       */
      
      #include <iostream>
      #include <vector>
      #include <algorithm> // 必须包含,用于 std::sort
      #include <cmath>     // 用于 std::max
      
      using namespace std;
      
      int main() {
          // 1. I/O 提速:关闭同步,对于大数据量(N=10^5)输入很有必要
          ios_base::sync_with_stdio(false);
          cin.tie(NULL);
      
          int L, N;
          if (!(cin >> L >> N)) return 0;
      
          // 使用 vector 动态数组存储切割点坐标
          // 预分配内存是个好习惯,避免 push_back 扩容带来的开销
          vector<int> cuts(N);
      
          for (int i = 0; i < N; ++i) {
              cin >> cuts[i];
          }
      
          // --- 关键步骤一:排序 ---
          // 题目明确说“坐标不一定有序”,而我们需要计算相邻切点的距离。
          // 所以必须先排序。复杂度 O(N log N)。
          sort(cuts.begin(), cuts.end());
      
          int max_len = 0;
      
          // --- 关键步骤二:计算线性部分的相邻距离 ---
          // 遍历排序后的数组,计算 cuts[i] 和 cuts[i-1] 之间的距离
          for (int i = 1; i < N; ++i) {
              int current_segment = cuts[i] - cuts[i-1];
              // 更新最大值
              if (current_segment > max_len) {
                  max_len = current_segment;
              }
          }
      
          // --- 关键步骤三:处理环形接口(易错点) ---
          // 环的最后一段是从 最后一个切点(cuts[N-1]) 到 0,再从 0 到 第一个切点(cuts[0])
          // 距离公式 = (总周长 - 最后一个坐标) + 第一个坐标
          int tail_to_head = (L - cuts[N-1]) + cuts[0];
          
          if (tail_to_head > max_len) {
              max_len = tail_to_head;
          }
      
          // 输出结果
          cout << max_len << endl;
      
          return 0;
      }
      

      代码详细分析

      1. 易错点与关键点注释

      • 排序的重要性 (std::sort)
        • 这是解题的前提。如果输入序列是 80, 10, 35,直接相减是没有任何物理意义的。必须变为 10, 35, 80 才能计算中间片段。
      • 首尾相连的处理
        • 很多同学容易漏掉最后一段。
        • 代码中的 (L - cuts[N-1]) 代表从最后一个点走到终点(即起点0)的距离。
        • + cuts[0] 代表从起点0走到第一个点的距离。
        • 两段加起来才是跨越零点的那个片段长度。
      • 数据类型
        • 题目中 L1,000,000,000L \le 1,000,000,000 (10910^9)。C++ 中 int 的最大值约为 2×1092 \times 10^9。所以使用 int 是安全的,不需要 long long(除非涉及加和可能超过范围,但这里是求最大单个长度,肯定小于 LL)。

      2. 时间和空间复杂度分析

      空间复杂度分析

      • 我们需要存储 NN 个切割点坐标。
      • 使用了 vector<int>,大小为 NN
      • 结论O(N)O(N)。对于 N=100,000N=100,000,内存占用极小(约 400KB),远低于竞赛通常的 256MB 限制。

      时间复杂度分析思考过程

      1. 输入阶段:读取 NN 个整数,复杂度 O(N)O(N)
      2. 排序阶段:这是程序中最耗时的部分。std::sort 使用的是快速排序/堆排序/插入排序的混合(Introsort),平均和最坏复杂度均为 O(NlogN)O(N \log N)
      3. 扫描阶段:遍历数组计算差值,复杂度 O(N)O(N)
      4. 总运算量
        • 主要由排序决定:NlogNN \log N
        • N=105N=10^5 时,log2N17\log_2 N \approx 17
        • 运算次数 1.7×106\approx 1.7 \times 10^6,远小于 1秒限时(通常可处理 10810^8 次运算)。
      5. 结论O(NlogN)O(N \log N)。程序运行非常快。

      3. 时间复杂度优化建议

      对于本题,O(NlogN)O(N \log N) 已经是标准最优解

      • 为什么不能 O(N)O(N)
        • 因为要在数轴上计算相邻距离,本质上依赖于元素的有序性。在基于比较的排序模型中,下界就是 NlogNN \log N
      • 非主流优化(基数排序)
        • 如果非常极端地追求 O(N)O(N),可以使用基数排序(Radix Sort)。因为坐标是整数。
        • 但是!LL 高达 10910^9,如果用桶排序会爆内存。用基数排序虽然理论是 O(N×k)O(N \times k),但常数较大且代码复杂,在 N=105N=10^5 这个量级下,不一定比高度优化的 std::sort 快多少,且容易写错。
        • 竞赛建议:直接用 std::sort,稳准狠。

      模拟数据示例(草稿纸验证)

      输入:

      100 3
      80 10 35
      
      1. 读入cuts = {80, 10, 35}
      2. 排序cuts 变为 {10, 35, 80}
      3. 循环扫描
        • i=1i=1: cuts[1] - cuts[0] = 3510=2535 - 10 = 25max_len = 25
        • i=2i=2: cuts[2] - cuts[1] = 8035=4580 - 35 = 45max_len = 45
      4. 首尾处理
        • L=100L = 100, cuts.back() = 80, cuts.front() = 10
        • 距离 = (10080)+10=20+10=30(100 - 80) + 10 = 20 + 10 = 30
        • 比较 max(45, 30),结果保持 45。
      5. 输出:45。正确。
      • 1

      信息

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