2 条题解

  • 0
    @ 2025-12-16 17:26:39

    这是一个非常实用的需求。为了方便你搭建OJ(Online Judge)的测试数据,我编写了一个全自动的C++数据生成器

    这个程序运行时,会在当前目录下直接生成 1.in ~ 10.in 以及对应的 1.out ~ 10.out 文件。

    数据生成策略设计

    为了保证测试数据的全面性和强度,我设计了如下的测试点分布,覆盖了边界情况、特殊构造和大数据压力测试:

    • 测试点 1-3 (小数据/边界)
      • #1: N=2N=2 (题目最小限制),验证极小情况。
      • #2: N=100N=100,完全随机。
      • #3: N=100N=100,数值全部相等(平地),验证特殊逻辑。
    • 测试点 4-6 (中等数据 N1000N \le 1000)
      • #4: 单调递增序列。
      • #5: 单调递减序列。
      • #6: 随机分布。
    • 测试点 7-10 (大数据 N=105N = 10^5)
      • #7: 凹型数据(两端高,中间低),考察贪心是否会被两端误导。
      • #8: 凸型数据(两端低,中间高),考察向内收缩的正确性。
      • #9: 锯齿状数据(0, 10000, 0, 10000...),考察极端波动。
      • #10: 全随机大数据,最大强度压测。

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

    请将以下代码保存为 generator.cpp,编译并运行即可。

    /**
     * NOI Style Data Generator for "Container With Most Water"
     * Generates 1.in/1.out to 10.in/10.out
     * 
     * Compiler: C++14
     * Author: OI Coach
     */
    
    #include <iostream>
    #include <vector>
    #include <string>
    #include <fstream>
    #include <random>
    #include <algorithm>
    #include <chrono>
    
    using namespace std;
    
    // ==========================================
    // Part 1: 标准解法 (Solver)
    // 用于生成 .out 文件,确保答案绝对正确
    // ==========================================
    long long solve(const vector<int>& a) {
        int n = a.size();
        int L = 0;
        int R = n - 1;
        long long max_area = 0;
        
        while (L < R) {
            int width = R - L;
            int h = min(a[L], a[R]);
            max_area = max(max_area, (long long)width * h);
            
            if (a[L] < a[R]) {
                L++;
            } else {
                R--;
            }
        }
        return max_area;
    }
    
    // ==========================================
    // Part 2: 数据生成器核心 (Generator)
    // ==========================================
    
    // 随机数生成器初始化 (使用 mt19937 保证随机质量)
    mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
    
    // 生成指定范围内的随机整数 [min, max]
    int random_int(int min, int max) {
        uniform_int_distribution<int> dist(min, max);
        return dist(rng);
    }
    
    // 不同类型的数据构造函数
    // type 0: 完全随机
    // type 1: 单调递增
    // type 2: 单调递减
    // type 3: 全部相等
    // type 4: 凹型 (两头高中间低)
    // type 5: 凸型 (两头低中间高)
    // type 6: 锯齿型 (高低交错)
    vector<int> generate_data(int n, int max_val, int type) {
        vector<int> a(n);
        
        if (type == 0) { // Random
            for (int i = 0; i < n; ++i) a[i] = random_int(0, max_val);
        } 
        else if (type == 1) { // Increasing
            for (int i = 0; i < n; ++i) a[i] = (long long)i * max_val / n;
        }
        else if (type == 2) { // Decreasing
            for (int i = 0; i < n; ++i) a[i] = max_val - ((long long)i * max_val / n);
        }
        else if (type == 3) { // All Same
            int val = random_int(1, max_val);
            for (int i = 0; i < n; ++i) a[i] = val;
        }
        else if (type == 4) { // Concave (Bowl)
            for (int i = 0; i < n; ++i) {
                int mid = n / 2;
                int dist = abs(i - mid);
                a[i] = (long long)dist * max_val / mid; 
                if (a[i] == 0) a[i] = 1; // 避免0
            }
        }
        else if (type == 5) { // Convex (Mountain)
            for (int i = 0; i < n; ++i) {
                int mid = n / 2;
                int dist = abs(i - mid);
                a[i] = max_val - ((long long)dist * max_val / mid);
            }
        }
        else if (type == 6) { // Sawtooth
            for (int i = 0; i < n; ++i) {
                a[i] = (i % 2 == 0) ? random_int(0, max_val / 10) : random_int(max_val * 0.9, max_val);
            }
        }
    
        return a;
    }
    
    void write_file(int id, int n, int max_val, int type, const string& desc) {
        string in_filename = to_string(id) + ".in";
        string out_filename = to_string(id) + ".out";
        
        // 生成数据
        vector<int> a = generate_data(n, max_val, type);
        
        // 写入 .in 文件
        ofstream fin(in_filename);
        fin << n << "\n";
        for (int i = 0; i < n; ++i) {
            fin << a[i] << (i == n - 1 ? "" : " ");
        }
        fin << "\n";
        fin.close();
        
        // 计算答案并写入 .out 文件
        long long ans = solve(a);
        ofstream fout(out_filename);
        fout << ans << "\n";
        fout.close();
        
        cout << "Generated Test Case #" << id << ": " << desc 
             << " (N=" << n << ", Type=" << type << ")" << endl;
    }
    
    int main() {
        cout << "Start generating data..." << endl;
    
        // 题目约束
        const int MAX_VAL = 10000;
        const int MAX_N_SMALL = 1000;
        const int MAX_N_LARGE = 100000;
    
        // --- 测试点 1-3: 小数据与边界 ---
        write_file(1, 2, 100, 0, "Smallest N");           // N=2, 随机
        write_file(2, 100, MAX_VAL, 0, "Small Random");   // N=100, 随机
        write_file(3, 100, MAX_VAL, 3, "All Same Values");// N=100, 全等
    
        // --- 测试点 4-6: 中等数据 (满足 30% 数据范围) ---
        write_file(4, MAX_N_SMALL, MAX_VAL, 1, "Increasing"); // 单调增
        write_file(5, MAX_N_SMALL, MAX_VAL, 2, "Decreasing"); // 单调减
        write_file(6, MAX_N_SMALL, MAX_VAL, 0, "Mid Random"); // 中等随机
    
        // --- 测试点 7-10: 大数据 (满足 100% 数据范围) ---
        write_file(7, MAX_N_LARGE, MAX_VAL, 4, "Concave (Bowl)");     // 凹型
        write_file(8, MAX_N_LARGE, MAX_VAL, 5, "Convex (Mountain)");  // 凸型
        write_file(9, MAX_N_LARGE, MAX_VAL, 6, "Sawtooth (Extreme)"); // 锯齿
        write_file(10, MAX_N_LARGE, MAX_VAL, 0, "Large Random");      // 满级随机
    
        cout << "All data generated successfully!" << endl;
        return 0;
    }
    

    使用指南

    1. 编译
      g++ generator.cpp -o generator -O2 -std=c++14
      
    2. 运行
      • 在 Windows 上双击 generator.exe 或命令行运行。
      • 在 Linux/Mac 上运行 ./generator
    3. 结果: 程序运行结束后,你的文件夹里会出现 1.in, 1.out ... 10.in, 10.out 共20个文件。

    代码说明

    1. 非递归写法: 数据生成和求解过程全部采用迭代(循环)写法,避免了 N=105N=10^5 时递归可能导致的栈溢出风险。
    2. 效率优化
      • 求解器使用的是 O(N)O(N) 的双指针算法。对于 10 组数据,总计算量约为 5×1055 \times 10^5 次操作,运行时间在几毫秒以内,非常快。
      • vector 预分配内存,IO 使用 ofstream,确保生成效率。
    3. 随机性: 使用了 std::mt19937 配合 chrono 时间种子,比 rand() 产生的随机数质量更高,分布更均匀。
    4. 格式规范
      • 输入文件:符合题目要求的第一行 NN,第二行空格分隔的数组。
      • 输出文件:单个整数结果,带换行符。

    你可以直接把这些文件上传到你的 OJ 后台作为标准测试集。

    • 0
      @ 2025-12-16 17:23:32

      你好!作为你的教练,很高兴看到你开始关注代码实现的规范性和细节。在NOI/NOIP竞赛中,算法的正确性固然重要,但代码的鲁棒性(Robustness)数据类型的选择以及常数优化往往决定了你是拿满分还是遗憾挂分。

      下面是针对第11题“盛最多水的容器”的标准题解代码,完全符合NOI C++14标准。

      1. 标准题解代码 (C++14)

      /**
       * 题目:盛水容器 (Water Container)
       * 考察点:双指针 (Two Pointers)、贪心 (Greedy)
       * 时间复杂度:O(n)
       * 空间复杂度:O(n)
       * 
       * 作者:OI金牌教练
       * 编译器标准:C++14
       */
      
      #include <iostream>
      #include <vector>
      #include <algorithm> // 用于 std::max, std::min
      
      using namespace std;
      
      // 【关键点1】数据规模与类型定义
      // 虽然本题最大乘积约为 10^5 * 10^4 = 10^9,正好在 int (约2*10^9) 范围内。
      // 但在竞赛中,为了防止边界溢出或数据加强,涉及“乘积”、“累加”的结果,
      // 强烈建议习惯性使用 long long。
      using ll = long long;
      
      // 【关键点2】全局数组定义
      // 对于 N=10^5 级别的数据,尽量开在全局变量或使用 vector。
      // 如果在 main 函数内部开 int a[100005],可能会导致栈溢出 (Stack Overflow)。
      const int MAXN = 100005;
      int a[MAXN]; 
      
      int main() {
          // 【关键点3】I/O 加速
          // 必须关闭流同步,否则在大数据量读入时(>5*10^5)可能会因为IO过慢而超时。
          // 注意:关闭后不能混用 scanf/printf 和 cin/cout。
          ios::sync_with_stdio(false);
          cin.tie(nullptr);
      
          int n;
          if (!(cin >> n)) return 0;
      
          // 读入数据,这里使用 1-based indexing (下标从1开始) 还是 0-based 都可以。
          // 为了方便计算宽度 (j - i),这里采用 0 到 n-1。
          for (int i = 0; i < n; ++i) {
              cin >> a[i];
          }
      
          // 【关键点4】双指针初始化
          // L 代表左挡板下标,R 代表右挡板下标
          int L = 0;
          int R = n - 1;
          
          ll max_area = 0; // 记录最大面积
      
          // 【关键点5】循环终止条件
          // 当 L 和 R 相遇时,容器宽度为0,没有意义,结束循环。
          while (L < R) {
              // 计算当前底边宽度
              int width = R - L;
              
              // 计算当前高度(短板效应:取两者较小值)
              int h = min(a[L], a[R]);
              
              // 更新答案
              // 注意:计算时强制转为 long long,防止中间结果溢出(虽然本题不会,但要养成习惯)
              max_area = max(max_area, (ll)width * h);
      
              // 【关键点6】贪心策略核心
              // 我们希望面积 = 宽度 * 高度 最大化。
              // 现在的宽度已经是当前可能的最大值了(随着指针内移,宽度只会减小)。
              // 想要面积变大,必须指望高度变高。
              // 如果移动高板:宽度减小,新板子再高也受限于没动的那个短板,高度不会增加,面积一定变小。
              // 如果移动短板:宽度减小,但新板子可能很高,从而让整体高度提升,面积有可能变大。
              // 结论:谁短谁移动。
              if (a[L] < a[R]) {
                  L++; // 左边短,左指针右移
              } else {
                  R--; // 右边短(或相等),右指针左移
              }
          }
      
          cout << max_area << endl;
      
          return 0;
      }
      

      2. 复杂度分析与思考过程

      时间复杂度分析

      • 思考过程: 我们主要关注代码中循环的执行次数。 我们定义了两个指针 LR,分别位于数组的两端。在 while 循环的每一步中,要么 L 增加 1,要么 R 减少 1。 循环终止条件是 L == R。这意味着 LR 加起来总共移动了 n1n-1 步。 循环内部的操作(比较、计算、赋值)都是 O(1)O(1) 的常数操作。
      • 结论:总时间复杂度为 O(n)O(n)
        • 对于 n=105n = 10^5 的数据,计算机大约需要执行 10510^5 次运算,远小于竞赛通常的 1秒限时(约 10810^8 次运算),因此该算法非常高效,可以通过 100%100\% 的数据。

      空间复杂度分析

      • 思考过程: 我们开了一个大小为 nn 的数组 a[] 来存储输入数据。 除此之外,只使用了 L, R, max_area, width, h 等几个辅助变量。
      • 结论:总空间复杂度为 O(n)O(n)
        • 10510^5int 大约占用 400KB 内存,远小于通常的 128MB 或 256MB 内存限制。

      3. 时间复杂度优化建议

      对于这道题,O(n)O(n) 已经是理论上的最优解了,因为你至少需要读取 nn 个数(这是 O(n)O(n) 的)。算法层面上无法进一步优化时间复杂度,但我们可以在常数层面(Constant Factor)进行微调,这在卡常题(Time Limit 非常紧的题目)中很有用:

      1. I/O 优化(最重要)

        • 如代码所示,使用 ios::sync_with_stdio(false); cin.tie(nullptr);
        • 如果数据量极大(例如 n106n \ge 10^6),可以使用 快读 (Fast Read) 模板(即使用 getchar() 解析整数),比 cinscanf 都要快。
      2. 避免不必要的内存操作

        • 代码中使用了全局数组 int a[MAXN]。相比于 std::vector<int> a(n),静态数组通常略微快一点点,因为它没有动态分配内存和构造对象的开销(虽然这个差异微乎其微)。
      3. 分支预测优化(高级,本题不强求)

        • CPU 流水线不喜欢 if-else 分支跳转。但在双指针算法中,分支是必须的逻辑,难以避免。有些同学会尝试用位运算消除分支,但在这种业务逻辑下往往得不偿失。保持代码逻辑清晰(如上的贪心判断)是首选。

      4. 教练的总结(易错点复盘)

      1. 关于 long long

        • 现象:很多同学看到 ai104a_i \le 10^4 就觉得用 int 足够了。
        • 教训:在OI比赛中,出题人有时会把数据范围卡在 int 溢出的边缘(例如 21092 \cdot 10^9 以上)。凡是涉及“乘法”计算出结果的,请无脑上 long long,这不会增加多少运行时间,但能买个保险。
      2. 贪心的证明

        • 为什么移动长的那一边不行?一定要自己在草稿纸上画图确认:移动长边,短板不变(甚至更短),宽度减小 \rightarrow 面积必减小。这个逻辑闭环是解题的关键。

      希望这份标准题解能帮助你规范代码习惯!继续加油!

      • 1

      信息

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