2 条题解

  • 0
    @ 2025-12-16 17:15:14

    你好!作为金牌教练,这就为你提供一套专业的 OJ数据生成器 (Data Generator)

    这套代码集成了两部分:

    1. 标准解法 (Solver):使用前文提到的 O(N2)O(N^2) 中心扩展法,保证生成准确的 .out 答案。
    2. 数据构造逻辑 (Generator):精心设计了 10 个测试点,覆盖了从极小值、边界值、特殊构造(如全字符相同、无回文、长回文)到最大数据范围的随机数据。

    你可以直接编译运行此 C++ 代码,它会在当前目录下自动生成 1.in/1.out10.in/10.out 共20个文件。

    数据生成器 C++ 代码

    /*
     * Title: OJ Data Generator for Longest Palindromic Substring
     * Description: Generates 10 sets of input (.in) and output (.out) files.
     * Format: NOI Standard (C++14)
     * Author: OI Coach
     */
    
    #include <iostream>
    #include <fstream>
    #include <string>
    #include <vector>
    #include <algorithm>
    #include <random>
    #include <chrono>
    
    using namespace std;
    
    // ==========================================
    // Part 1: 标准解法 (Solver)
    // 用于生成 .out 文件
    // ==========================================
    
    int expand(const string& s, int left, int right) {
        while (left >= 0 && right < s.size() && s[left] == s[right]) {
            left--;
            right++;
        }
        return right - left - 1;
    }
    
    string solve(const string& s) {
        if (s.empty()) return "";
        int start = 0, maxLen = 0;
        int n = s.size();
        for (int i = 0; i < n; ++i) {
            int len1 = expand(s, i, i);
            int len2 = expand(s, i, i + 1);
            int len = max(len1, len2);
            if (len > maxLen) {
                maxLen = len;
                start = i - (len - 1) / 2;
            }
        }
        return s.substr(start, maxLen);
    }
    
    // ==========================================
    // Part 2: 数据构造工具 (Generator Tools)
    // ==========================================
    
    // 字符集:数字 + 大小写字母
    const string CHARSET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
    
    // 随机数生成器初始化
    mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
    
    // 生成指定长度的随机字符串
    string genRandomString(int len) {
        string s = "";
        uniform_int_distribution<int> dist(0, CHARSET.size() - 1);
        for (int i = 0; i < len; ++i) {
            s += CHARSET[dist(rng)];
        }
        return s;
    }
    
    // 生成全相同的字符串
    string genAllSame(int len, char c) {
        return string(len, c);
    }
    
    // 生成从不重复的字符串 (abcde...),最大回文长度为1
    string genNonPalindrome(int len) {
        string s = "";
        for (int i = 0; i < len; ++i) {
            // 循环使用 a-z,避免相邻或间隔重复
            s += (char)('a' + (i % 26)); 
        }
        return s;
    }
    
    // 生成一个巨大的回文串 (abc...cba)
    string genGiantPalindrome(int len) {
        int halfLen = len / 2;
        string half = genRandomString(halfLen);
        string s = half;
        if (len % 2 == 1) s += CHARSET[rng() % CHARSET.size()]; // 中间加一个随机字符
        string revHalf = half;
        reverse(revHalf.begin(), revHalf.end());
        s += revHalf;
        return s;
    }
    
    // ==========================================
    // Part 3: 主程序 (Main)
    // ==========================================
    
    int main() {
        // 定义10个测试点的生成策略
        for (int i = 1; i <= 10; ++i) {
            string input_str;
            string desc; // 描述(用于调试)
    
            // --- 数据构造逻辑 ---
            switch(i) {
                case 1: 
                    // 样例数据
                    input_str = "babad"; 
                    desc = "Sample Case";
                    break;
                case 2: 
                    // 极小边界:长度1
                    input_str = "Z"; 
                    desc = "Boundary: Len 1";
                    break;
                case 3: 
                    // 极小边界:长度2,相同 (偶数回文)
                    input_str = "OO"; 
                    desc = "Boundary: Len 2 (Same)";
                    break;
                case 4: 
                    // 极小边界:长度2,不同
                    input_str = "Op"; 
                    desc = "Boundary: Len 2 (Diff)";
                    break;
                case 5: 
                    // 小规模随机数据
                    input_str = genRandomString(50); 
                    desc = "Random Small (Len 50)";
                    break;
                case 6: 
                    // 特殊情况:全相同字符 (最大回文=全长)
                    input_str = genAllSame(1000, 'A'); 
                    desc = "All Same Chars (Len 1000)";
                    break;
                case 7: 
                    // 特殊情况:无长度>1的回文串
                    input_str = genNonPalindrome(1000); 
                    desc = "No Palindrome > 1 (Len 1000)";
                    break;
                case 8: 
                    // 特殊情况:整个字符串就是回文
                    input_str = genGiantPalindrome(1000); 
                    desc = "Whole Palindrome (Len 1000)";
                    break;
                case 9: 
                    // 特殊情况:多峰值 (多个等长最长回文)
                    // 构造 "x...xAAAAAx...xBBBBBx...x"
                    // 用 'x' 填充,中间插入固定的回文
                    {
                        string filler1 = genAllSame(300, 'x');
                        string filler2 = genAllSame(300, 'y'); // 避免和 filler1 合并
                        string filler3 = genAllSame(300, 'z');
                        // "12345" 和 "54321" 长度均为5
                        input_str = filler1 + "123454321" + filler2 + "abcdeedcba" + filler3;
                        // 注意:这里的长度可能略微超过1000,稍微截断一下符合题意
                        if(input_str.size() > 1000) input_str.resize(1000);
                    }
                    desc = "Multiple Max Peaks";
                    break;
                case 10: 
                    // 大规模随机强测
                    input_str = genRandomString(1000); 
                    desc = "Random Large (Len 1000)";
                    break;
            }
    
            // --- 写入输入文件 (.in) ---
            string inFileName = to_string(i) + ".in";
            ofstream inFile(inFileName);
            if (!inFile) { cerr << "Error creating " << inFileName << endl; return 1; }
            inFile << input_str;
            inFile.close();
    
            // --- 计算并写入输出文件 (.out) ---
            string outFileName = to_string(i) + ".out";
            ofstream outFile(outFileName);
            if (!outFile) { cerr << "Error creating " << outFileName << endl; return 1; }
            
            // 运行标准解法
            string ans = solve(input_str);
            
            outFile << ans; // NOI格式通常文末可以有换行,也可以没有,string自带无换行,若需换行可加 << endl
            outFile.close();
    
            cout << "Generated Test Case " << i << ": " << desc << " [Len: " << input_str.length() << "]" << endl;
        }
    
        cout << "\nAll 10 test cases generated successfully." << endl;
        return 0;
    }
    

    数据点设计说明 (Coverage Analysis)

    1. Test 1 (Sample): babad。直接使用题目样例,确保基础理解正确。
    2. Test 2 (Boundary Min): Z。长度为1的边界情况。
    3. Test 3 (Boundary Even): OO。长度为2且字符相同,测试偶数长度回文的最简情况。
    4. Test 4 (Boundary Diff): Op。长度为2且字符不同,测试无回文扩展的情况。
    5. Test 5 (Random Small): 长度50的随机串。测试常规逻辑。
    6. Test 6 (Max Pattern): 1000个 A。测试算法在最坏情况下的性能(每次都要扩展到头),以及输出是否为全长。
    7. Test 7 (Min Pattern): abcde...。无任何长度大于1的回文串,测试算法是否能正确返回单个字符作为答案。
    8. Test 8 (Max Palindrome): 1000长度的整体回文(如 ab...ba)。测试大跨度回文的识别。
    9. Test 9 (Multi Peak): 人工构造包含两个独立的最长回文(例如中间有 123454321abcdeedcba)。测试题目中“输出任意一个”的要求(标准答案代码会输出先找到的那个,或者根据逻辑输出其中之一,只要符合长度即可)。
    10. Test 10 (Random Max): 长度1000的完全随机串。模拟真实比赛中的强测数据。

    使用方法

    1. 将上述代码保存为 generator.cpp
    2. 使用 g++ 编译:g++ generator.cpp -o generator -std=c++14
    3. 运行:./generator (Windows下是 generator.exe)。
    4. 程序结束后,你的文件夹里会出现 1.in~10.in1.out~10.out
    • 0
      @ 2025-12-16 17:13:56

      你好!作为你的教练,我很高兴看到你准备进入代码实现的阶段。在考场上,代码的规范性、边界处理的严谨性往往决定了你能不能拿满分。

      对于数据范围 N=1000N=1000 的题目,中心扩展法 (Center Expansion) 是最优选的“标准解法”。相比于动态规划 (DP),它不需要额外的 O(N2)O(N^2) 空间,且常数更小,实际运行速度极快。

      以下是符合 NOIP/NOI 竞赛风格的 C++14 标准代码。

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

      /*
       * 题目: 最长回文子串 (Longest Palindromic Substring)
       * 算法: 中心扩展法 (Expand Around Center)
       * 时间复杂度: O(N^2)
       * 空间复杂度: O(1)
       * 
       * 考点: 字符串处理、枚举、边界判断
       */
      
      #include <iostream>
      #include <string>
      #include <algorithm>
      #include <vector>
      
      using namespace std;
      
      // 辅助函数:以 left 和 right 为中心向两边扩展
      // 如果 left == right,说明以字符为中心(寻找奇数长度回文)
      // 如果 left + 1 == right,说明以空隙为中心(寻找偶数长度回文)
      int expand(const string& s, int left, int right) {
          // 关键点1:边界检查必须写在访问 s[left] 和 s[right] 之前,利用 && 的短路特性防止越界
          while (left >= 0 && right < s.size() && s[left] == s[right]) {
              left--;
              right++;
          }
          // 易错点1:当循环结束时,s[left] != s[right] 或者越界。
          // 此时回文串的范围是 [left + 1, right - 1]。
          // 长度计算:(right - 1) - (left + 1) + 1 = right - left - 1
          return right - left - 1;
      }
      
      int main() {
          // 竞赛必备:关闭流同步,加速 I/O
          ios::sync_with_stdio(false);
          cin.tie(nullptr);
      
          string s;
          if (!(cin >> s)) return 0; // 防止读取失败
      
          int n = s.size();
          if (n < 2) {
              cout << s << endl;
              return 0;
          }
      
          int start = 0;   // 记录最长回文串的起始下标
          int maxLen = 0;  // 记录最长回文串的长度
      
          for (int i = 0; i < n; ++i) {
              // 情况A:以当前字符 s[i] 为中心(如 "aba") -> 奇数长度
              int len1 = expand(s, i, i);
              
              // 情况B:以 s[i] 和 s[i+1] 之间的空隙为中心(如 "abba") -> 偶数长度
              int len2 = expand(s, i, i + 1);
              
              int len = max(len1, len2);
      
              // 关键点2:只有发现更长的回文串时才更新
              if (len > maxLen) {
                  maxLen = len;
                  // 易错点2:根据中心位置 i 和长度 len 推导起始位置 start
                  // 举例推导:
                  // 1. 奇数 "aba", i=1, len=3 -> start=0. 公式: 1 - (3-1)/2 = 0
                  // 2. 偶数 "abba", i=1, len=4 -> start=0. 公式: 1 - (4-1)/2 = 1 - 1 = 0 (注意整除)
                  // 结论:统一公式为 i - (len - 1) / 2
                  start = i - (len - 1) / 2; 
              }
          }
      
          // 关键点3:不要在循环里做 substr,那样会导致 O(N^3)
          // 只在最后输出时截取一次
          cout << s.substr(start, maxLen) << endl;
      
          return 0;
      }
      

      二、 复杂度分析思考过程

      1. 时间复杂度分析

      • 思考:代码有一层 for 循环遍历字符串的每个字符,这是 O(N)O(N)。在循环内部,我们调用了 expand 函数。
      • 最坏情况:当字符串全是同一个字符时(如 aaaaa...),对于每一个中心,expand 都会向两边扩展直到边界。这意味着每次扩展需要 O(N)O(N) 的时间。
      • 计算:总时间复杂度 = 外部循环次数 ×\times 内部扩展次数 = O(N)×O(N)=O(N2)O(N) \times O(N) = O(N^2)
      • 数据范围验证:题目给出 N1000N \le 1000N2=106N^2 = 10^6,计算机一秒大概能跑 10810^8 次运算,所以 10610^6 是非常安全的,完全可以通过。

      2. 空间复杂度分析

      • 思考:我们需要多少额外的内存?
      • 分析:我们只用了几个整型变量 (i, start, maxLen, left, right, len),没有开辟额外的数组或者递归栈。
      • 结论:空间复杂度为 O(1)O(1)(不计算存储输入字符串本身的 O(N)O(N) 空间)。这比动态规划解法的 O(N2)O(N^2) 空间要优秀得多。

      三、 进阶优化建议 (针对 N 大很多的情况)

      教练提示:虽然上面的代码能过这道题,但如果题目数据范围加强到 N105N \le 10^510710^7,上面的 O(N2)O(N^2) 就会超时 (TLE)

      这时候你需要掌握Manacher 算法 (马拉车算法)

      • 核心思想:利用回文串的对称性,避免重复计算。如果我们知道大回文串里包含了一个小回文串,那么在这个大回文串对称的另一边,通常也有一个相同的小回文串,不需要重新扩展。
      • 复杂度:时间 O(N)O(N),空间 O(N)O(N)
      • NOI 考场策略
        • 首选O(N2)O(N^2) 中心扩展法。因为代码短、好写、不易出错,且 N=1000N=1000 时足够快。
        • 备选:只有在明确数据范围 N>3000N > 3000 时,才考虑写 Manacher,因为那个算法细节繁琐,考场上写错的风险较大。

      希望这份标准代码和分析能帮你彻底吃透这道题!把它写在你的代码库里,经常复习其中的边界计算公式。加油!

      • 1

      信息

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