2 条题解

  • 0
    @ 2025-12-16 17:06:06

    这是一个完整的测试数据生成器(Data Generator)。 当你编译并运行这段代码后,它会在当前目录下自动生成 1.in ~ 10.in1.out ~ 10.out 文件。

    设计策略总结

    为了确保测试数据的全面性强度,我设计了如下 10 个测试点:

    1. 基础测试:短小简单的回文串(全小写)。
    2. 基础测试:短小简单的非回文串。
    3. 标准样例:题目给出的经典大小写+符号混合回文。
    4. 标准样例:题目给出的经典非回文。
    5. 边界情况:全是非法字符(符号、空格),清洗后为空串,应判为 True。
    6. 边界情况:单个字符,应判为 True。
    7. 压力测试:全部由相同字符组成的中等长度字符串(全 'A')。
    8. 性能测试:最大长度 (2×1052 \times 10^5) 的纯字母数字回文串,考察 O(N)O(N) 性能。
    9. 性能测试:最大长度的几乎回文串(只有中间不同),考察算法是否准确比较。
    10. 综合极限测试:最大长度 (2×1052 \times 10^5),包含大量随机符号、大小写混合的回文串,考察解析复杂度和鲁棒性。

    Generator 代码 (C++14)

    /*
     * Problem: Valid Palindrome - Data Generator
     * Standard: C++14
     * Output: Generates 1.in/out to 10.in/out
     * Author: OI Coach
     */
    
    #include <iostream>
    #include <fstream>
    #include <string>
    #include <vector>
    #include <random>
    #include <algorithm>
    #include <cctype>
    #include <chrono>
    
    using namespace std;
    
    // ==========================================
    // 1. 核心解题逻辑 (用于生成 .out 文件)
    // ==========================================
    bool solve_logic(const string& s) {
        int n = s.length();
        int left = 0, right = n - 1;
        while (left < right) {
            while (left < right && !isalnum(s[left])) left++;
            while (left < right && !isalnum(s[right])) right--;
            if (left < right) {
                if (tolower(s[left]) != tolower(s[right])) return false;
                left++;
                right--;
            }
        }
        return true;
    }
    
    // ==========================================
    // 2. 随机数生成器配置
    // ==========================================
    mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
    
    char get_alnum() {
        const string charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
        return charset[rng() % charset.size()];
    }
    
    char get_garbage() {
        const string charset = " !@#$%^&*()_+-=[]{};':\",./<>?";
        return charset[rng() % charset.size()];
    }
    
    // 随机改变字符大小写
    char randomize_case(char c) {
        if (isalpha(c)) {
            return (rng() % 2) ? tolower(c) : toupper(c);
        }
        return c;
    }
    
    // ==========================================
    // 3. 字符串构造函数
    // ==========================================
    
    // 生成指定长度的纯随机字符串
    string gen_random_string(int len) {
        string s = "";
        for(int i=0; i<len; ++i) {
            if (rng() % 10 < 7) s += get_alnum(); // 70% 几率是字母数字
            else s += get_garbage();
        }
        return s;
    }
    
    // 生成回文串 (包含噪音)
    // len: 目标大约长度
    // noise_ratio: 噪音比例 (0.0 - 1.0)
    string gen_palindrome(int len, double noise_ratio) {
        string raw_half = "";
        int valid_len = len * (1.0 - noise_ratio);
        if (valid_len < 1) valid_len = 1;
        
        // 1. 生成一半有效字符
        for (int i = 0; i < valid_len / 2; i++) {
            raw_half += get_alnum();
        }
        
        // 2. 构造回文核心
        string core = raw_half;
        string reversed_half = raw_half;
        reverse(reversed_half.begin(), reversed_half.end());
        
        // 中间可能加一个单独字符
        if (valid_len % 2 != 0) core += get_alnum();
        core += reversed_half;
        
        // 3. 混入噪音并随机变大小写
        string res = "";
        int core_idx = 0;
        int current_len = 0;
        
        // 简单的插入逻辑:在还没放完core之前,随机决定放噪音还是放core
        while (current_len < len && core_idx < core.length()) {
            double r = (double)(rng() % 1000) / 1000.0;
            if (r < noise_ratio) {
                res += get_garbage();
            } else {
                res += randomize_case(core[core_idx++]);
            }
            current_len++;
        }
        // 补齐剩余的 core
        while (core_idx < core.length()) {
            res += randomize_case(core[core_idx++]);
        }
        
        return res;
    }
    
    // 生成 "几乎" 回文串 (中间破坏,确保 False)
    string gen_broken_palindrome(int len, double noise_ratio) {
        string s = gen_palindrome(len, noise_ratio);
        
        // 找到中间的有效字符并破坏它
        // 提取有效字符位置
        vector<int> alnum_indices;
        for (int i = 0; i < s.length(); i++) {
            if (isalnum(s[i])) alnum_indices.push_back(i);
        }
        
        if (!alnum_indices.empty()) {
            // 修改中间位置附近的字符,使其不再回文
            int mid = alnum_indices.size() / 2;
            int target_idx = alnum_indices[mid];
            
            char original = s[target_idx];
            char changed = original;
            
            // 确保改后的字符不同,且仍然是有效字符
            do {
                changed = get_alnum();
            } while (tolower(changed) == tolower(original));
            
            s[target_idx] = changed;
        } else {
            // 如果全是噪音,强制加个 'a' 'b' 
            s = "ab"; 
        }
        
        return s;
    }
    
    // ==========================================
    // 4. 文件写入辅助
    // ==========================================
    void write_test_case(int id, string content) {
        string in_name = to_string(id) + ".in";
        string out_name = to_string(id) + ".out";
        
        // 写入 .in
        ofstream fin(in_name);
        fin << content; 
        // 注意:有些题目要求行末换行,这里不强制加 endl,保持原始数据
        // 但为了通用性,通常字符串题不需要末尾特定换行,除非用 getline
        // 这里我们只输出内容。
        fin.close();
        
        // 写入 .out
        ofstream fout(out_name);
        bool ans = solve_logic(content);
        fout << (ans ? "true" : "false"); // 不加 endl,防止部分对比工具报错,或者根据习惯加 endl
        fout << endl; // 加上换行符合一般 OJ 习惯
        fout.close();
        
        cout << "Generated Test Case " << id << " [" << (ans ? "True" : "False") << "] Size: " << content.length() << endl;
    }
    
    // ==========================================
    // 5. 主程序:生成 10 个测试点
    // ==========================================
    int main() {
        cout << "Start generating test data..." << endl;
    
        // Case 1: 基础简单回文 (True)
        write_test_case(1, "aba");
    
        // Case 2: 基础简单非回文 (False)
        write_test_case(2, "abc");
    
        // Case 3: 题目样例 1 (混合大小写 + 符号) (True)
        write_test_case(3, "A man, a plan, a canal: Panama");
    
        // Case 4: 题目样例 2 (False)
        write_test_case(4, "race a car");
    
        // Case 5: 边界 - 纯符号/空格 (空串视为 True)
        write_test_case(5, ".,?!  ;;  //");
    
        // Case 6: 边界 - 单字符 (True)
        write_test_case(6, "z");
    
        // Case 7: 压力 - 全是相同字符,中等长度 (True)
        // 构造 1000 个 'A'
        write_test_case(7, string(1000, 'A'));
    
        // Case 8: 极限 - 纯字母数字,超长回文 (True)
        // 长度 200,000,无噪音
        write_test_case(8, gen_palindrome(200000, 0.0));
    
        // Case 9: 极限 - 纯字母数字,超长 "伪" 回文 (False)
        // 长度 200,000,中间破坏,最坏情况需扫描到中间
        write_test_case(9, gen_broken_palindrome(200000, 0.0));
    
        // Case 10: 综合 - 极限长度 + 大量噪音 (True)
        // 长度 200,000,噪音比例 60%,有效字符约 80,000
        // 这种不仅考察逻辑,还考察跳过字符的效率
        write_test_case(10, gen_palindrome(200000, 0.6));
    
        cout << "All 10 test cases generated successfully." << endl;
        return 0;
    }
    

    代码说明

    1. 覆盖率

      • 1-2:最基本的功能验证。
      • 3-4:原题样例验证。
      • 5-6:极小输入和空串逻辑验证(易错点)。
      • 7:相同字符验证。
      • 8:时间复杂度上限验证(纯计算量)。
      • 9:最坏情况验证(必须比较完所有字符才能发现中间不同)。
      • 10:复杂逻辑验证(大量的非字母数字字符需要跳过,测试 isalnum 和循环跳跃的逻辑)。
    2. 生成逻辑

      • gen_palindrome: 先生成一半随机串,再镜像翻转,保证逻辑上的回文。然后随机将大小写打乱(Aa),并在随机位置插入随机的“垃圾字符”(空格、标点),以此模拟真实复杂的输入。
      • gen_broken_palindrome: 基于回文生成,但强制修改中间的一个有效字符,确保它是 False,且算法必须运行到中间才能发现错误(最大化运行时间)。
    3. IO 处理

      • 使用了 std::ofstream 直接输出到文件。
      • 内置了标准解法(solve_logic),在生成 .in 的同时直接计算出正确的 .out,保证数据的一致性,无需人工干预。

    你可以直接复制并在本地 C++ 环境中运行此代码,即可得到全套 OJ 测试数据。

    • 0
      @ 2025-12-16 17:02:13

      这是一个符合 NOIP/NOI C++14 竞赛标准的题解代码。

      在竞赛中,代码的鲁棒性(防越界)、效率(IO优化)和规范性(标准库的使用)是得分的关键。

      核心代码 (C++14 Standard)

      /*
       * 题目: 验证回文串 (Valid Palindrome)
       * 算法: 双指针 (Two Pointers)
       * 复杂度: 时间 O(N), 空间 O(1)
       * 作者: OI金牌教练
       */
      
      #include <iostream>
      #include <string>
      #include <vector>
      #include <cctype> // 包含 isalnum, tolower 等字符处理函数
      
      using namespace std;
      
      // 解决问题的核心函数
      bool solve() {
          string s;
          // 关键点1:输入整行字符串
          // cin >> s 遇到空格会停止,必须使用 getline 读取包含空格的整行文本
          if (!getline(cin, s)) {
              return true; // 防御性编程:如果读不到数据,视为空串处理
          }
      
          int n = s.length();
          if (n == 0) return true; // 空串定义为回文串
      
          int left = 0;
          int right = n - 1;
      
          // 双指针相向移动
          while (left < right) {
              // 关键点2:寻找左边下一个有效字符
              // 易错点:必须加上 left < right 判断,防止全是符号时指针越界(Segment Fault)
              while (left < right && !isalnum(s[left])) {
                  left++;
              }
      
              // 关键点3:寻找右边下一个有效字符
              while (left < right && !isalnum(s[right])) {
                  right--;
              }
      
              // 关键点4:比较字符
              // 此时 s[left] 和 s[right] 都是字母或数字,且 left <= right
              if (left < right) {
                  // 统一转小写比较
                  if (tolower(s[left]) != tolower(s[right])) {
                      return false; // 一旦发现不对称,立即判定为否
                  }
                  // 匹配成功,继续向中间靠拢
                  left++;
                  right--;
              }
          }
      
          return true; // 所有位置检查通过
      }
      
      int main() {
          // 关键点5:IO 性能优化
          // 关闭 stdio 与 iostream 的同步,加快 cin/cout 速度,在大数据量下非常重要
          ios::sync_with_stdio(false);
          cin.tie(0);
      
          if (solve()) {
              cout << "true" << endl;
          } else {
              cout << "false" << endl;
          }
      
          return 0;
      }
      

      复杂度分析与思考过程

      在竞赛草稿纸上,你应该写下以下分析,以确定你的算法是否会超时(TLE)或超内存(MLE)。

      1. 时间复杂度分析

      • 思考过程
        • 我们需要遍历整个字符串来检查字符。
        • 使用了两个指针 leftright
        • left 从 0 开始只增不减,rightn-1 开始只减不增。
        • 两个指针最终会在中间相遇。
        • 这意味着字符串中的每一个字符,最多只会被访问常数次(判断是否有效、进行比较)。
      • 结论:时间复杂度为 O(N)O(N),其中 NN 是字符串的长度。
      • 可行性:题目范围 N2×105N \le 2 \times 10^5
        • 计算机 1 秒通常能处理 10810^8 次运算。
        • 2×1052 \times 10^5 远小于 10810^8,所以 O(N)O(N) 算法绝对能通过,甚至 O(NlogN)O(N \log N) 也能过,但 O(N2)O(N^2) 会超时。

      2. 空间复杂度分析

      • 思考过程
        • 我们只使用了 s (存储输入), n, left, right (几个整型变量)。
        • 没有申请额外的数组(如 vector 或新的 string)来存储处理后的字符串。
        • 这是一次“原地”(In-place)操作。
      • 结论:空间复杂度为 O(1)O(1)(不计算存储原始输入所需的空间)。这是空间最优解。

      时间复杂度优化建议

      虽然 O(N)O(N) 已经是理论最优,但在极端卡常数(Time Limit 非常紧)的情况下,或者是为了展示更深厚的功底,可以考虑以下优化细节:

      1. 手写字符判断函数: 虽然 <cctype>isalnum 很快,但在某些特定环境下,库函数可能有包含 locale(本地化)检查的开销。你可以手写一个简单的宏或内联函数:

        // 手写判断,避免库函数调用开销
        inline bool myIsAlnum(char c) {
            return (c >= '0' && c <= '9') || 
                   (c >= 'a' && c <= 'z') || 
                   (c >= 'A' && c <= 'Z');
        }
        
        // 手写转小写,利用 ASCII 码特性(大写转小写 +32)
        inline char myToLower(char c) {
            if (c >= 'A' && c <= 'Z') return c + 32;
            return c;
        }
        

        注意:在现代编译器(如 GCC 开了 -O2 优化)下,库函数通常会被内联优化得非常好,手写优化的提升微乎其微,但在嵌入式或极古老的判题系统中可能有效。

      2. 避免反复调用 s.length(): 在 while 循环条件中,尽量不要写 i < s.length(),因为有些编译器每次循环都会去计算一次长度。虽然 std::string::length() 通常是 O(1)O(1) 的,但提取出来赋值给 int n 是个好习惯。

      3. 引用传参: 如果将核心逻辑封装在函数里,传参时尽量使用 const string &s 而不是 string s

        • string s:会发生一次字符串拷贝,时间 O(N)O(N),空间 O(N)O(N)
        • const string &s:只是传引用,几乎无开销。
        • 注:本题解直接在 solvemain 中处理,未涉及函数传参拷贝问题。
      • 1

      信息

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