2 条题解
-
0
好的,教练来了!
为《字符串的排列》这道题制作一套高质量的测试数据,关键在于覆盖各种边界情况和可能导致错误逻辑的陷阱。例如,s1比s2长、全匹配、部分匹配、首尾匹配、以及“看起来像但差一点”的迷惑性数据。
以下是为你准备的数据生成器,它将创建 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 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 文件 (标准滑动窗口解法) bool solve(const string& s1, const string& s2) { int n = s1.length(); int m = s2.length(); if (n > m) return false; vector<int> cnt1(26, 0); vector<int> cnt2(26, 0); for (int i = 0; i < n; ++i) { cnt1[s1[i] - 'a']++; cnt2[s2[i] - 'a']++; } if (cnt1 == cnt2) return true; for (int i = n; i < m; ++i) { cnt2[s2[i] - 'a']++; cnt2[s2[i - n] - 'a']--; if (cnt1 == cnt2) return true; } return false; } // 保存测试点文件 void saveTestCase(int caseNum, const string& s1, const string& s2) { string inName = to_string(caseNum) + ".in"; string outName = to_string(caseNum) + ".out"; // 1. 写入 .in 文件 ofstream fin(inName); fin << s1 << "\n"; fin << s2 << "\n"; fin.close(); // 2. 计算答案并写入 .out 文件 bool result = solve(s1, s2); ofstream fout(outName); fout << (result ? "true" : "false") << "\n"; fout.close(); cout << "Generated Case " << caseNum << ": |s1|=" << s1.length() << ", |s2|=" << s2.length() << ", Answer=" << (result ? "true" : "false") << endl; } int main() { // 【测试点 1】:小规模数据,答案为 true { string s1 = "abc"; string s2 = "xyzcbaxyz"; saveTestCase(1, s1, s2); } // 【测试点 2】:小规模数据,答案为 false { string s1 = "ab"; string s2 = "axby"; // 字符存在但非连续 saveTestCase(2, s1, s2); } // 【测试点 3】:边界情况 - s1 的长度大于 s2 { string s1 = randomString(100); string s2 = randomString(50); saveTestCase(3, s1, s2); } // 【测试点 4】:边界情况 - s1 和 s2 完全相同 { string s = randomString(1000); saveTestCase(4, s, s); } // 【测试点 5】:边界情况 - s1 是 s2 的一个排列 (n=m) { string s1 = randomString(1000); string s2 = s1; shuffle(s2.begin(), s2.end(), rng); saveTestCase(5, s1, s2); } // 【测试点 6】:大规模数据 - 匹配项在 s2 的开头 { int n = 5000, m = 10000; string s1 = randomString(n); string s2 = s1; shuffle(s2.begin(), s2.end(), rng); // s2 是 s1 的一个排列 s2 += randomString(m - n); // 在后面接上随机串 saveTestCase(6, s1, s2); } // 【测试点 7】:大规模数据 - 匹配项在 s2 的末尾 { int n = 5000, m = 10000; string s1 = randomString(n); string permutation_s1 = s1; shuffle(permutation_s1.begin(), permutation_s1.end(), rng); string s2 = randomString(m - n) + permutation_s1; saveTestCase(7, s1, s2); } // 【测试点 8】:特殊构造 - “几乎”匹配的 false 情况 // s2 中有一个窗口,与 s1 只差一个字符 { int n = 10000, m = 10000; string s1 = randomString(n); string s2 = s1; shuffle(s2.begin(), s2.end(), rng); // 确保 s1 和 s2 不完全相同 if (s1 == s2) s1[0] = (s1[0] == 'a' ? 'b' : 'a'); // 在 s2 的最后一个字符上做手脚,使其不匹配 if (s2.back() == 'z') s2.back() = 'y'; else s2.back()++; saveTestCase(8, s1, s2); } // 【测试点 9】:特殊构造 - s1 中有大量重复字符 { int n = 1000, m = 5000; string s1(n, 'a'); s1[n / 2] = 'b'; // s1 = "aaaa...b...aaaa" string s2 = randomString(2000) + string(n, 'a') + randomString(m - n - 2000); // s2 中有连续的 n 个 'a',不匹配 s1 saveTestCase(9, s1, s2); } // 【测试点 10】:大规模随机数据,答案随机 { int n = 8000, m = 10000; string s1 = randomString(n); string s2 = randomString(m); // 随机决定是否要插入一个 true 的答案 if (randomInt(0, 1) == 1) { string p = s1; shuffle(p.begin(), p.end(), rng); int pos = randomInt(0, m - n); s2.replace(pos, n, p); } saveTestCase(10, s1, s2); } cout << "All 10 test cases generated successfully!" << endl; return 0; }数据覆盖点说明表
这份生成器精心设计了10个测试点,旨在从不同角度考察代码的正确性和鲁棒性。
| 测试点编号 |
|s1|||s2|| 数据特征 | 考察目的 | 预期答案 | | :--- | :--- | :--- | :--- | :--- | :--- | | 1 | 3 | 9 | 小数据,匹配在中间 | 基础功能验证 |true| | 2 | 2 | 4 | 小数据,字符存在但非连续 | 区分“包含字符”与“子串是排列” |false| | 3 | 100 | 50 ||s1| > |s2|| 边界健壮性:能否处理不可能情况 |false| | 4 | 1000 | 1000 |s1 == s2| 边界情况:本身即是自己的排列 |true| | 5 | 1000 | 1000 |s2是s1的排列 | 边界情况:|s1| == |s2||true| | 6 | 5000 | 10000 | 匹配项在s2开头 | 滑动窗口的初始状态检查 |true| | 7 | 5000 | 10000 | 匹配项在s2结尾 | 滑动窗口的最终状态检查 |true| | 8 | 10000 | 10000 |s2是s1排列后改动一字符 | 迷惑性数据:测试计数是否精确 |false| | 9 | 1000 | 5000 |s1含大量重复字符 | 特殊构造:测试频率统计的准确性 |false| | 10 | 8000 | 10000 | 大规模随机(可能true/false) | 综合压力测试 | 随机 |使用方法
- 将代码保存为
gen.cpp。 - 使用 C++14 编译器编译:
g++ gen.cpp -o gen -std=c++14 - 运行生成器:
./gen - 程序运行后,当前目录下会生成
1.in,1.out,2.in,2.out, ... ,10.in,10.out共 20 个文件,可以直接用于你的 OJ 系统。
- 将代码保存为
-
0
你好!我是你的OI竞赛教练。
针对《字符串的排列》这道题,它是滑动窗口算法的经典入门题。在NOI系列竞赛中,处理字符串子串匹配、统计类问题时,这种“定长窗口滑动+哈希统计”的方法是必须掌握的基本功。
下面提供符合 NOIP C++14 标准的题解代码,并附带详细的复杂度分析。
一、 标准题解代码 (C++14)
/* * 题目:字符串的排列 (Permutation in String) * 算法:滑动窗口 (Sliding Window) + 计数数组 * 时间复杂度:O(N),其中 N 为 s2 的长度 (常数系数为 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 s1, s2; if (!(cin >> s1 >> s2)) return 0; int n = s1.length(); int m = s2.length(); // 【易错点1】边界情况处理 // 如果 s1 比 s2 还要长,s2 显然不可能包含 s1 的排列 if (n > m) { cout << "false" << endl; return 0; } // 定义计数数组,大小为26对应 'a'-'z' // 使用 vector<int> 方便利用重载的 == 运算符进行全等比较 vector<int> cnt1(26, 0); vector<int> cnt2(26, 0); // 1. 预处理:统计 s1 的字符频率,以及 s2 中第一个窗口的字符频率 for (int i = 0; i < n; ++i) { cnt1[s1[i] - 'a']++; cnt2[s2[i] - 'a']++; } // 检查第一个窗口是否匹配 if (cnt1 == cnt2) { cout << "true" << endl; return 0; } // 2. 开始滑动窗口 // 窗口长度固定为 n。 // 当前窗口范围是 [i - n + 1, i],我们需要维护这个范围内的计数 for (int i = n; i < m; ++i) { // 【关键点2】进出原则 // 新进入窗口的字符:s2[i] cnt2[s2[i] - 'a']++; // 移出窗口的字符:s2[i - n] // 例如:窗口长度为3,i=3时,移出的是下标为0的字符 cnt2[s2[i - n] - 'a']--; // 【关键点3】检查匹配 // 这里的比较时间复杂度是 O(26),也就是 O(1) if (cnt1 == cnt2) { cout << "true" << endl; return 0; } } // 遍历结束仍未找到 cout << "false" << endl; return 0; }
二、 复杂度分析思考过程
在考场上,写完代码后必须在草稿纸上简单推算一下复杂度,以确定是否会超时。
1. 时间复杂度分析
- 思考步骤:
- 初始化阶段:遍历了 (长度 ),复杂度 。
- 滑动阶段:循环从 到 ,执行了约 次。
- 循环内部操作:
- 数组索引访问、自增自减:。
- 关键操作:
if (cnt1 == cnt2)。在 C++ STL 中,比较两个长度为 的vector,底层会遍历这 个元素。因此单次比较是 ,即 ,其中 是字符集大小。
- 总复杂度:。
- 结论:由于 是常数,时间复杂度为线性 (即 )。
- 数据规模验证:题目给出的范围是 。 算法大约运行 次指令,远小于 的每秒运算上限,因此 1.0s 内可以通过。
2. 空间复杂度分析
- 思考步骤:
- 我们需要存储输入字符串 。
- 额外申请了两个大小为 26 的固定数组
cnt1和cnt2。
- 结论:辅助空间与输入规模无关,空间复杂度为 。
三、 时间复杂度优化建议 (进阶)
虽然上述 的解法已经足够通过本题,但在字符集很大(例如 ASCII 256 或 Unicode)或者数据规模极大()的情况下,我们可以进一步优化掉那个常数 26。
优化思路:维护
diff变量 我们可以维护一个变量diff,表示当前窗口cnt2与目标cnt1有多少个字母的计数不一致。- 如果
diff == 0,说明找到了排列。 - 每次窗口滑动时,只更新进来的字符和出去的字符对应的计数,并动态更新
diff。- 例如:如果进来的字符让
cnt2[x]从“不等于cnt1[x]”变成了“等于”,则diff--。 - 反之,如果破坏了相等关系,则
diff++。
- 例如:如果进来的字符让
优化后的核心逻辑 (Snippet):
// 仅供参考的 O(m) 严格优化版逻辑 // 初始化 diff int diff = 0; for (int i = 0; i < 26; ++i) { if (cnt1[i] != cnt2[i]) diff++; } for (int i = n; i < m; ++i) { if (diff == 0) return true; // 可以在循环开始处判断上一轮结果 int inChar = s2[i] - 'a'; int outChar = s2[i - n] - 'a'; // 如果进来的字符和出去的字符一样,diff 不变,cnt 也不用变(抵消了) if (inChar == outChar) continue; // 处理进来的字符 if (cnt2[inChar] == cnt1[inChar]) diff++; // 原本相等,现在多了,变得不等了 cnt2[inChar]++; if (cnt2[inChar] == cnt1[inChar]) diff--; // 加上后相等了 // 处理出去的字符 if (cnt2[outChar] == cnt1[outChar]) diff++; // 原本相等,现在少了,变得不等了 cnt2[outChar]--; if (cnt2[outChar] == cnt1[outChar]) diff--; // 减去后相等了 } return diff == 0;教练建议: 在 NOI/CSP 考场上,首选第一种
vector比较的写法。因为它代码逻辑简单、不易出错(Bug-free Is Key)。除非题目明确卡常数或者字符集巨大,否则不要轻易增加逻辑复杂度去写优化版。 - 思考步骤:
- 1
信息
- ID
- 19346
- 时间
- 1000ms
- 内存
- 128MiB
- 难度
- 10
- 标签
- (无)
- 递交数
- 1
- 已通过
- 1
- 上传者