2 条题解
-
0
没问题,教练来了!
为《找到字符串中所有字母异位词》这道题出数据,重点在于构造性和边界性。我们需要测试窗口在开头、结尾、中间、重叠等各种情况下的表现,同时也要有迷惑性的数据(“差一点”就是异位词)。
以下是一个全面的数据生成器,可以生成 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 | 大规模随机数据,植入答案 | 综合压力测试,模拟真实比赛中的复杂随机数据 |使用说明
- 将代码保存为
gen.cpp。 - 在终端中使用 C++14 编译器进行编译:
g++ gen.cpp -o gen -std=c++14。 - 执行生成器:
./gen。 - 执行完毕后,当前目录下就会生成
1.in,1.out到10.in,10.out的所有测试文件,可以直接部署到你的 OJ 平台。
- 将代码保存为
-
0
你好!我是你的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. 时间复杂度分析
- 思考过程:
- 初始化阶段:我们遍历了字符串
p一次,同时遍历了s的前m(|p|) 个字符。这部分的计算量是 。 - 滑动阶段:主循环
for (int i = m; i < n; ++i)从m遍历到n-1,共执行了 次。 - 循环内部:
- 更新计数的两行代码
s_window_cnt[...]++和s_window_cnt[...]--都是 的操作。 - 比较两个
vector(p_cnt == s_window_cnt),需要逐个比较 26 个元素,所以是 ,即 ,其中 是字符集大小。
- 更新计数的两行代码
- 总和:。
- 初始化阶段:我们遍历了字符串
- 结论:因为 是一个常数,且 ,所以总时间复杂度简化为 。
- 数据验证:对于 ,总操作数大约是 ,在 1 秒的时限内非常安全。
2. 空间复杂度分析
- 思考过程:
- 我们声明了哪些和输入规模相关的变量?
p_cnt和s_window_cnt的大小都是 26,是固定的,与n,m无关。ans向量用于存储结果。在最坏情况下,例如s="ababab",p="ab",ans的大小会接近n。
- 结论:
- 辅助空间复杂度(算法运行时必须的额外空间):,即 。
- 输出空间复杂度(存储答案所需的空间):最坏情况下是 。在竞赛中,通常我们关心的是前者。
三、 时间复杂度优化建议
当前解法的时间复杂度为 ,虽然在 时可以看作 ,但在学术上或面对更大字符集时,可以进行常数优化。
优化思路:差异化统计 我们可以不每次都完整比较两个数组,而是维护一个“差异值”,来判断两个数组是否相等。
-
维护
diff变量:- 初始化两个计数数组后,计算出
p_cnt和s_window_cnt中有多少个位置的计数不相等,记为diff。 - 当
diff == 0时,两个数组完全相等。
- 初始化两个计数数组后,计算出
-
动态更新
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,做类似处理。
- 当窗口滑动时,我们只更新了两个字符的计数。我们只需要检查这两个字符的计数变化是否影响了
优化效果: 通过这种方式,每次循环内部的判断从 降为了 ,使得总时间复杂度从 严格变为 。
教练建议: 在标准的 NOIP/CSP 竞赛中,由于字符集通常很小(如26个小写字母),直接比较
vector的写法是最推荐的。它代码简洁,不易出错,且性能完全足够。diff优化是一种优秀的思想,适合在面试或对理论复杂度要求更高的场合展现,但在争分夺秒的竞赛中,代码的正确性和编写速度优先。 - 思考过程:
- 1
信息
- ID
- 19347
- 时间
- 1000ms
- 内存
- 128MiB
- 难度
- 10
- 标签
- (无)
- 递交数
- 1
- 已通过
- 1
- 上传者