2 条题解

  • 0
    @ 2025-12-17 15:39:17

    没问题,教练来了!

    为《找到字符串中所有字母异位词》这道题出数据,重点在于构造性和边界性。我们需要测试窗口在开头、结尾、中间、重叠等各种情况下的表现,同时也要有迷惑性的数据(“差一点”就是异位词)。

    以下是一个全面的数据生成器,可以生成 10 个高质量的测试点。

    数据生成器代码 (Data Generator)

    /**
     * 题目:找到字符串中所有字母异位词 - 数据生成器
     * 用途:生成测试点 1.in/out ~ 10.in/out
     * 标准:C++14
     * 策略:覆盖基础、边界、重叠、首尾匹配、大规模等情况
     */
    
    #include <iostream>
    #include <fstream>
    #include <vector>
    #include <string>
    #include <random>
    #include <algorithm>
    #include <chrono>
    
    using namespace std;
    
    // 全局高质量随机数引擎
    mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
    
    // 生成指定范围内的随机整数 [min, max]
    int randomInt(int min, int max) {
        uniform_int_distribution<int> dist(min, max);
        return dist(rng);
    }
    
    // 生成一个指定长度的随机小写字母字符串
    string randomString(int len) {
        string s(len, 'a');
        for (int i = 0; i < len; ++i) {
            s[i] = (char)('a' + randomInt(0, 25));
        }
        return s;
    }
    
    // 核心解题逻辑:用于生成 .out 文件 (标准滑动窗口解法)
    vector<int> solve(const string& s, const string& p) {
        int n = s.length();
        int m = p.length();
        vector<int> ans;
        if (m > n) return ans;
    
        vector<int> p_cnt(26, 0);
        vector<int> s_cnt(26, 0);
    
        for (int i = 0; i < m; ++i) {
            p_cnt[p[i] - 'a']++;
            s_cnt[s[i] - 'a']++;
        }
    
        if (p_cnt == s_cnt) ans.push_back(0);
    
        for (int i = m; i < n; ++i) {
            s_cnt[s[i] - 'a']++;
            s_cnt[s[i - m] - 'a']--;
            if (p_cnt == s_cnt) {
                ans.push_back(i - m + 1);
            }
        }
        return ans;
    }
    
    // 保存测试点文件
    void saveTestCase(int caseNum, const string& s, const string& p) {
        string inName = to_string(caseNum) + ".in";
        string outName = to_string(caseNum) + ".out";
    
        // 1. 写入 .in 文件
        ofstream fin(inName);
        fin << s << "\n";
        fin << p << "\n";
        fin.close();
    
        // 2. 计算答案并写入 .out 文件
        vector<int> result = solve(s, p);
        ofstream fout(outName);
        for (int i = 0; i < result.size(); ++i) {
            fout << result[i] << (i == result.size() - 1 ? "" : " ");
        }
        fout << "\n";
        fout.close();
    
        cout << "Generated Case " << caseNum << ": |S|=" << s.length() 
             << ", |P|=" << p.length() << ", Found " << result.size() << " anagrams." << endl;
    }
    
    int main() {
        // 【测试点 1】:样例 1
        saveTestCase(1, "cbaebabacd", "abc");
    
        // 【测试点 2】:样例 2 - 重叠情况
        saveTestCase(2, "abab", "ab");
    
        // 【测试点 3】:边界情况 - P 比 S 长
        saveTestCase(3, "a", "ab");
    
        // 【测试点 4】:边界情况 - P 和 S 等长且是异位词
        {
            string p = "zyxw";
            string s = "wxyz";
            saveTestCase(4, s, p);
        }
        
        // 【测试点 5】:边界情况 - P 和 S 等长但不是异位词
        {
            string p = randomString(10000);
            string s = randomString(10000);
            saveTestCase(5, p, s);
        }
    
        // 【测试点 6】:大规模数据 - 匹配项在 S 的开头
        {
            int m = 5000, n = 30000;
            string p = randomString(m);
            string anagram_p = p;
            shuffle(anagram_p.begin(), anagram_p.end(), rng);
            string s = anagram_p + randomString(n - m);
            saveTestCase(6, s, p);
        }
    
        // 【测试点 7】:大规模数据 - 匹配项在 S 的末尾
        {
            int m = 5000, n = 30000;
            string p = randomString(m);
            string anagram_p = p;
            shuffle(anagram_p.begin(), anagram_p.end(), rng);
            string s = randomString(n - m) + anagram_p;
            saveTestCase(7, s, p);
        }
    
        // 【测试点 8】:特殊构造 - 密集重叠匹配
        {
            int m = 3, n = 30000;
            string p = "aba";
            string s = "";
            for(int i=0; i < n/2; ++i) s += "ab"; // s = "ababab..."
            saveTestCase(8, s, p);
        }
    
        // 【测试点 9】:特殊构造 - "几乎"匹配的迷惑性数据 (全'a'串)
        {
            int m = 10000, n = 30000;
            string p(m, 'a'); // p = "aaaaa..."
            p.back() = 'b';  // p = "aaaa...b"
            string s(n, 'a'); // s = "aaaa...a"
            saveTestCase(9, s, p);
        }
    
        // 【测试点 10】:大规模随机数据,随机植入答案
        {
            int m = 5000, n = 30000;
            string p = randomString(m);
            string s = randomString(n);
            // 随机植入 3 个答案
            for (int k = 0; k < 3; ++k) {
                string anagram_p = p;
                shuffle(anagram_p.begin(), anagram_p.end(), rng);
                int pos = randomInt(0, n - m);
                s.replace(pos, m, anagram_p);
            }
            saveTestCase(10, s, p);
        }
        
        cout << "All 10 test cases generated successfully!" << endl;
        return 0;
    }
    

    数据覆盖点说明表

    | 测试点编号 | |S|, |P| | 数据特征 | 考察目的 | | :--- | :--- | :--- | :--- | | 1 | 10, 3 | 样例数据 | 基础功能验证 | | 2 | 4, 2 | 样例数据 | 重叠匹配的简单情况 | | 3 | 1, 2 | |P| > |S| | 边界健壮性:能否正确处理不可能的情况 | | 4 | 4, 4 | |P| == |S|, 是异位词 | 边界情况:整个字符串就是一个匹配 | | 5 | 10k, 10k | |P| == |S|, 非异位词 | 边界情况:大规模下的不匹配 | | 6 | 30k, 5k | 匹配项在 S 开头 | 测试滑动窗口的初始化逻辑是否正确 | | 7 | 30k, 5k | 匹配项在 S 末尾 | 测试滑动窗口的循环终止条件和最后一步的判断 | | 8 | 30k, 3 | S="abab...", P="aba" | 密集重叠,测试窗口滑动的每一步 | | 9 | 30k, 10k | S全a, P是aaa...b | 迷惑性数据:每个窗口都"几乎"匹配,测试计数的精确性 | | 10 | 30k, 5k | 大规模随机数据,植入答案 | 综合压力测试,模拟真实比赛中的复杂随机数据 |

    使用说明

    1. 将代码保存为 gen.cpp
    2. 在终端中使用 C++14 编译器进行编译:g++ gen.cpp -o gen -std=c++14
    3. 执行生成器:./gen
    4. 执行完毕后,当前目录下就会生成 1.in, 1.out10.in, 10.out 的所有测试文件,可以直接部署到你的 OJ 平台。
    • 0
      @ 2025-12-17 15:36:35

      你好!我是你的OI竞赛教练。

      这道《找到字符串中所有字母异位词》是滑动窗口技巧的直接应用,考察的是将一个“判断是否存在”的问题扩展为“找到所有符合条件的位置”的能力。代码结构与前一题高度相似,但对细节的要求更高。

      以下是符合 NOIP C++14 标准的题解代码,并附带详细的分析。

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

      /*
       * 题目:找到字符串中所有字母异位词 (Find All Anagrams in a String)
       * 算法:滑动窗口 (Sliding Window) + 计数数组
       * 时间复杂度:O(|S|),其中 |S| 是主串长度 (常数系数为 26)
       * 空间复杂度:O(1) (辅助空间)
       * 编写标准:NOIP/CSP C++14
       */
      
      #include <iostream>
      #include <vector>
      #include <string>
      
      using namespace std;
      
      int main() {
          // 【关键点1】I/O加速,竞赛必备
          ios::sync_with_stdio(false);
          cin.tie(nullptr);
      
          string s, p;
          if (!(cin >> s >> p)) return 0;
      
          int n = s.length();
          int m = p.length();
      
          vector<int> ans; // 存储结果索引
      
          // 【易错点1】边界条件:p 比 s 长,不可能找到异位词
          if (m > n) {
              cout << endl; // 按题目要求输出空行
              return 0;
          }
      
          // 计数数组,'a'-'z' 对应 0-25
          vector<int> p_cnt(26, 0);
          vector<int> s_window_cnt(26, 0);
      
          // 1. 初始化:统计 p 的字符频率,以及 s 的第一个窗口的频率
          for (int i = 0; i < m; ++i) {
              p_cnt[p[i] - 'a']++;
              s_window_cnt[s[i] - 'a']++;
          }
      
          // 2. 检查第一个窗口 (起始索引为 0)
          if (p_cnt == s_window_cnt) {
              ans.push_back(0);
          }
      
          // 3. 开始滑动窗口
          // 窗口的右边界从 m 滑动到 n-1
          for (int i = m; i < n; ++i) {
              // 【关键点2】窗口滑动:一进一出
              // a. 右边新字符进入窗口
              s_window_cnt[s[i] - 'a']++;
              
              // b. 左边旧字符离开窗口
              //    当前右边界是 i,窗口长度是 m,所以左边界是 i-m+1,
              //    要离开的字符是 i-m
              s_window_cnt[s[i - m] - 'a']--;
      
              // 4. 检查当前窗口是否匹配
              if (p_cnt == s_window_cnt) {
                  // 【易错点2】记录的是窗口的起始索引
                  // 当前窗口覆盖的范围是 [i - m + 1, i],起始索引是 i - m + 1
                  ans.push_back(i - m + 1);
              }
          }
      
          // 5. 按格式输出结果
          for (int i = 0; i < ans.size(); ++i) {
              cout << ans[i] << (i == ans.size() - 1 ? "" : " ");
          }
          cout << endl; // 即使 ans 为空,也要输出换行符以形成空行
      
          return 0;
      }
      

      二、 复杂度分析思考过程

      1. 时间复杂度分析

      • 思考过程
        1. 初始化阶段:我们遍历了字符串 p 一次,同时遍历了 s 的前 m (|p|) 个字符。这部分的计算量是 O(m)O(m)
        2. 滑动阶段:主循环 for (int i = m; i < n; ++i)m 遍历到 n-1,共执行了 nmn-m 次。
        3. 循环内部
          • 更新计数的两行代码 s_window_cnt[...]++s_window_cnt[...]-- 都是 O(1)O(1) 的操作。
          • 比较两个 vector (p_cnt == s_window_cnt),需要逐个比较 26 个元素,所以是 O(26)O(26),即 O(Σ)O(\Sigma),其中 Σ\Sigma 是字符集大小。
        4. 总和O(m)+(nm)×(O(1)+O(Σ))O(m) + (n-m) \times (O(1) + O(\Sigma))
      • 结论:因为 Σ=26\Sigma=26 是一个常数,且 mnm \le n,所以总时间复杂度简化为 O(n)O(n)
      • 数据验证:对于 n=3×104n=3 \times 10^4,总操作数大约是 3×104×267.8×1053 \times 10^4 \times 26 \approx 7.8 \times 10^5,在 1 秒的时限内非常安全。

      2. 空间复杂度分析

      • 思考过程
        • 我们声明了哪些和输入规模相关的变量?
        • p_cnts_window_cnt 的大小都是 26,是固定的,与 n, m 无关。
        • ans 向量用于存储结果。在最坏情况下,例如 s="ababab", p="ab"ans 的大小会接近 n
      • 结论
        • 辅助空间复杂度(算法运行时必须的额外空间):O(Σ)O(\Sigma),即 O(1)O(1)
        • 输出空间复杂度(存储答案所需的空间):最坏情况下是 O(n)O(n)。在竞赛中,通常我们关心的是前者。

      三、 时间复杂度优化建议

      当前解法的时间复杂度为 O(nΣ)O(n \cdot \Sigma),虽然在 Σ=26\Sigma=26 时可以看作 O(n)O(n),但在学术上或面对更大字符集时,可以进行常数优化。

      优化思路:差异化统计 我们可以不每次都完整比较两个数组,而是维护一个“差异值”,来判断两个数组是否相等。

      1. 维护 diff 变量

        • 初始化两个计数数组后,计算出 p_cnts_window_cnt 中有多少个位置的计数不相等,记为 diff
        • diff == 0 时,两个数组完全相等。
      2. 动态更新 diff

        • 当窗口滑动时,我们只更新了两个字符的计数。我们只需要检查这两个字符的计数变化是否影响了 diff
        • 对于进入的字符 c_in
          • 更新前 s_window_cnt[c_in] 的值。
          • 更新后 s_window_cnt[c_in] 的值。
          • 检查这两个值与 p_cnt[c_in] 的关系。例如,如果更新前 s_window_cnt[c_in] == p_cnt[c_in],而更新后不再相等,则 diff++。如果更新后恰好相等了,则 diff--
        • 对于离开的字符 c_out,做类似处理。

      优化效果: 通过这种方式,每次循环内部的判断从 O(Σ)O(\Sigma) 降为了 O(1)O(1),使得总时间复杂度从 O(nΣ)O(n \cdot \Sigma) 严格变为 O(n)O(n)

      教练建议: 在标准的 NOIP/CSP 竞赛中,由于字符集通常很小(如26个小写字母),直接比较 vector 的写法是最推荐的。它代码简洁,不易出错,且性能完全足够。diff 优化是一种优秀的思想,适合在面试或对理论复杂度要求更高的场合展现,但在争分夺秒的竞赛中,代码的正确性和编写速度优先

      • 1

      信息

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