2 条题解
-
0
这是一个完全符合 NOI 竞赛风格的数据生成器。它集成了标准解法(用于生成
.out文件)和针对不同测试点特性的数据构造逻辑(生成.in文件)。功能说明
- 全自动生成:运行后会在当前目录生成
1.in~10.in和1.out~10.out。 - 测试点覆盖:
- 1-2: 样例与基础小数据。
- 3-5: 边界情况(相等、无解、T比S长)。
- 6-7: 特殊字符分布(重复字符、全同字符)。
- 8: 中等规模随机。
- 9: 大规模数据,T 很短(测试滑动窗口大量移动)。
- 10: 大规模数据,T 很长(测试字符统计和匹配的负载)。
- 效率优化:算法为 ,生成 10 组数据仅需几十毫秒。
数据生成器代码 (C++14)
/** * NOI Style Data Generator for "Minimum Window Substring" * Compatible with C++14 */ #include <iostream> #include <fstream> #include <string> #include <vector> #include <algorithm> #include <random> #include <chrono> #include <climits> using namespace std; // ========================================== // Part 1: 标准题解函数 (Solver) // 用于生成 .out 文件,保证答案绝对正确 // ========================================== string solve(string s, string t) { if (s.empty() || t.empty()) return ""; // 统计 T 的字符需求 vector<int> need(128, 0); for (char c : t) need[c]++; int required_count = t.length(); int left = 0, right = 0; int min_len = INT_MAX; int start_pos = 0; int n = s.length(); while (right < n) { char c_in = s[right]; if (need[c_in] > 0) { required_count--; } need[c_in]--; right++; while (required_count == 0) { if (right - left < min_len) { min_len = right - left; start_pos = left; } char c_out = s[left]; need[c_out]++; if (need[c_out] > 0) { required_count++; } left++; } } return (min_len == INT_MAX) ? "" : s.substr(start_pos, min_len); } // ========================================== // Part 2: 数据生成工具库 // ========================================== mt19937 rng(chrono::steady_clock::now().time_since_epoch().count()); // 生成指定长度的随机字符串 (A-Z, a-z) string rand_string(int len) { string res; res.reserve(len); static const string charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; uniform_int_distribution<int> dist(0, charset.size() - 1); for (int i = 0; i < len; i++) { res += charset[dist(rng)]; } return res; } // 构造一个必定包含 T 的 S // 策略:先生成随机 S,然后将 T 的字符随机“撒”进去,保证有解 string make_solvable_s(int s_len, const string& t) { if (s_len < t.length()) s_len = t.length(); // 修正长度 string s = rand_string(s_len); // 创建一个包含所有 T 字符的索引列表 vector<int> indices(s_len); for(int i=0; i<s_len; ++i) indices[i] = i; shuffle(indices.begin(), indices.end(), rng); // 将 T 的字符覆盖到 S 的随机位置 for (size_t i = 0; i < t.length(); i++) { s[indices[i]] = t[i]; } return s; } // ========================================== // Part 3: 测试点生成逻辑 // ========================================== void generate_case(int id, string& s, string& t) { const int MAX_N = 100000; switch(id) { case 1: // 【样例数据】 s = "ADOBECODEBANC"; t = "ABC"; break; case 2: // 【基础随机】小数据 t = rand_string(5); s = make_solvable_s(20, t); break; case 3: // 【边界情况】S 和 T 完全相同 s = rand_string(100); t = s; break; case 4: // 【边界情况】S 比 T 短 (必无解) s = rand_string(10); t = rand_string(20); break; case 5: // 【边界情况】S 很长但不包含 T (无解) s = string(100, 'A'); t = "B"; break; case 6: // 【特殊构造】T 包含大量重复字符 t = "AAABBBCCC"; s = "XAABXBBXCCX"; // 干扰项很少,测试精确匹配 break; case 7: // 【退化测试】全字符相同,测试算法退化情况 t = string(50, 'A'); s = string(1000, 'A'); break; case 8: // 【中等规模】N = 5000, 保证有解 t = rand_string(100); s = make_solvable_s(5000, t); break; case 9: // 【大规模压力】N = 100,000, T 很短 // 测试滑动窗口在长串上的快速移动能力 t = rand_string(10); s = make_solvable_s(MAX_N, t); break; case 10: // 【大规模压力】N = 100,000, T 很长 (N/2) // 测试哈希表/计数器在大数据下的频繁更新效率 t = rand_string(MAX_N / 2); s = make_solvable_s(MAX_N, t); break; } } // ========================================== // Part 4: 主程序 // ========================================== int main() { printf("Generating data for Minimum Window Substring...\n"); for (int i = 1; i <= 10; i++) { string s, t; generate_case(i, s, t); // 写入 .in 文件 string in_filename = to_string(i) + ".in"; ofstream fout_in(in_filename); fout_in << s << "\n" << t << "\n"; fout_in.close(); // 计算答案并写入 .out 文件 string out_filename = to_string(i) + ".out"; ofstream fout_out(out_filename); string ans = solve(s, t); // 题目要求:如果不存在输出空行(对于C++ string来说就是输出""然后换行) // 如果 ans 为空,<< ans 输出空,再 << endl 换行,符合要求 fout_out << ans << endl; fout_out.close(); printf("Case %2d: Generated. |S|=%-6lu |T|=%-6lu AnsLen=%lu\n", i, s.length(), t.length(), ans.length()); } printf("Done. All 10 test cases generated.\n"); return 0; }使用指南
- 保存代码为
gen.cpp。 - 编译:
g++ gen.cpp -o gen -O2 -std=c++14。 - 运行:
./gen(Windows 下为gen.exe)。 - 程序将生成
1.in/1.out到10.in/10.out。
数据点详细设计
- Case 1 (Sample): 验证程序对原题样例的正确性。
- Case 2 (Small): 简单的随机小测,确保基本逻辑无误。
- Case 3 (Equality): 的情况,测试对完全覆盖的判断。
- Case 4 (Impossible Length): ,物理上不可能,答案应为空。
- Case 5 (Impossible Content): 字符种类不匹配,答案应为空。
- Case 6 (Repeated Chars): 中包含重复字符(如 "AAA"),测试计数器逻辑是否正确处理了重复需求。
- Case 7 (Degenerate): 全是 'A',测试算法在最坏情况下的性能(指针频繁移动)。
- Case 8 (Medium): 常规随机数据,。
- Case 9 (Large S, Small T): 模拟 是一个短单词,在海量文本 中搜索。这是最常见的应用场景。
- Case 10 (Large S, Large T): 极端情况,测试
need数组和required_count变量在高频操作下的稳定性。
所有数据均符合 的要求,且时间复杂度控制在秒级以内。
- 全自动生成:运行后会在当前目录生成
-
0
你好!这是为您准备的《最小覆盖子串》题解标准代码。代码完全遵循 NOIP/NOI C++14 竞赛标准,包含了详细的算法注释、复杂度分析以及针对竞赛场景的优化建议。
1. NOIP 标准参考代码 (C++14)
/* * 题目名称:最小覆盖子串 (Minimum Window Substring) * 算法核心:滑动窗口 (Sliding Window) / 双指针 * 编写风格:NOI/NOIP C++14 标准 * * 思路概要: * 1. 使用 right 指针主动向右扩展,寻找包含 T 的“可行窗口”。 * 2. 一旦窗口包含 T 中所有字符,停止 right,转而移动 left 指针收缩窗口。 * 3. 在保证窗口“合法”的前提下,让 left 尽可能右移,从而找到当前局部最优解。 * 4. 循环上述过程直到 right 到达终点。 */ #include <iostream> #include <string> #include <vector> #include <climits> // 包含 INT_MAX using namespace std; // 字符集大小,标准 ASCII 码为 0-127,开 128 足够。 // 如果题目涉及扩展 ASCII 或 Unicode,可能需要 map,但在 NOI 中通常数组最快。 const int MAX_CHAR = 128; int main() { // 【IO 优化】 // 关闭 cin/cout 与 stdin/stdout 的同步,极大提升读写速度 // 在数据量达到 10^5 时,这是 C++ 选手的基本操作 ios::sync_with_stdio(false); cin.tie(nullptr); string s, t; // 某些 OJ 或数据可能包含空格,若题目保证不含空格用 >> 即可,否则需用 getline // 本题 LeetCode 原题通常无空格,但若改为句子需注意 if (!(cin >> s >> t)) return 0; int n = s.length(); int m = t.length(); // 【特殊判定】 // 如果 S 比 T 还短,物理上不可能覆盖 if (n < m) { cout << "" << endl; // 输出空行 return 0; } // 【频次统计数组】 // need[c] 表示字符 c 在 T 中还需要的数量 // 正数:还需要;0:刚好够;负数:窗口里多余的 vector<int> need(MAX_CHAR, 0); for (char c : t) { need[c]++; } // required_count 表示还需要凑齐的 T 中字符的总个数 // 这里统计的是字符的“总数量”而非“种类数”,例如 T="AA",则 count=2 int required_count = m; int left = 0, right = 0; // 记录结果:最小长度和起始位置 int min_len = INT_MAX; int start_pos = 0; // 【滑动窗口主循环】 while (right < n) { char c_in = s[right]; // 即将进入窗口的字符 // 【关键点 1】逻辑判断顺序 // 如果当前字符 c_in 是 T 需要的(即 need > 0),那么有效计数减 1 if (need[c_in] > 0) { required_count--; } // 无论是否需要,都要把这个字符加入窗口(即需求量 -1) // 如果 need[c_in] 变成负数,说明窗口里这个字符已经过剩了 need[c_in]--; // 此时窗口右边界扩充完毕 right++; // 【收缩阶段】 // 当 required_count 为 0 时,说明当前窗口 [left, right) 已经是一个“可行解” // 我们尝试移动 left 来缩小窗口,看能否获得更优解 while (required_count == 0) { int current_len = right - left; // 更新全局最优解 if (current_len < min_len) { min_len = current_len; start_pos = left; } char c_out = s[left]; // 即将移出窗口的字符 // 【关键点 2】恢复计数 // 移出窗口,需求量 +1 need[c_out]++; // 如果移出后 need[c_out] 变为了正数,说明我们刚刚移出了一个“必需品” // 窗口不再合法(缺货了),循环将终止,必须继续移动 right 去进货 if (need[c_out] > 0) { required_count++; } left++; // 左指针右移 } } // 【输出结果】 if (min_len == INT_MAX) { // 未找到任何可行解 cout << "" << endl; } else { cout << s.substr(start_pos, min_len) << endl; } return 0; }
2. 复杂度分析与思考过程
在教学时,建议引导学生按照以下步骤思考复杂度的推导:
时间复杂度分析
- 思考误区:
学生看到代码里有
while (right < n),里面又套了一个while (required_count == 0),很容易误认为是 。 - 正确推导(摊还分析/势能分析):
- 我们要关注的是指针移动的次数,而不是循环嵌套的层数。
right指针:从 走到 ,每一个元素被访问一次,共移动 次。left指针:只能向右移动,无法回头。它最多也只能从 走到 ,共移动 次。- 对于每个字符,我们只进行了常数次操作(加减计数器、比较)。
- 总复杂度:,其中 是字符串 的长度。
空间复杂度分析
- 思考:我们需要存什么? 需要存储 中字符的统计信息。
- 推导:
我们使用了一个固定大小的数组
vector<int> need(128)。 无论 或 有多长,这个数组的大小是固定的(与字符集大小 有关)。 - 总复杂度: 或更准确地说是 。
3. 时间复杂度优化建议
虽然上述算法已经是 的最优解,但在竞赛实战的细节处理上,仍有优劣之分:
-
数组 vs 哈希表 (
std::map/std::unordered_map):- 建议:尽量使用数组(如
int count[128])。 - 原因:
unordered_map虽然理论平均 ,但实际常数巨大(涉及哈希计算、内存分配、桶冲突处理),在 级别的数据下,可能比数组慢 5-10 倍。对于 NOI 这种对时限敏感的比赛,直接开数组是最稳妥的。 - 特例:除非字符集非常大(例如 Unicode 字符),数组开不下,才考虑哈希表。
- 建议:尽量使用数组(如
-
IO 优化:
- 建议:必写
ios::sync_with_stdio(false); cin.tie(nullptr);。 - 原因:本题输入量可达 字符。默认的
cin因为要同步stdin缓冲区,速度较慢。关闭同步后,cin的速度足以媲美scanf,且处理string类型更方便。
- 建议:必写
-
避免频繁的
substr:- 建议:在循环过程中不要每次更新答案都调用
s.substr(left, len)。 - 原因:
substr会产生新的字符串对象,涉及内存分配和拷贝,复杂度是 。如果在循环里频繁调用,最坏情况会把总复杂度退化成 。 - 做法:只记录
start和len两个整数,最后 print 时再截取一次。
- 建议:在循环过程中不要每次更新答案都调用
4. 读题理解关键词(总结)
- “最小 / 最短” + “连续子串” 滑动窗口。
- “包含所有字符” 哈希表/数组计数器。
- “时间限制 1.0s” + “N=10^5” 必须设计 的算法,暴力 必死。
- 思考误区:
学生看到代码里有
- 1
信息
- ID
- 19345
- 时间
- 1000ms
- 内存
- 128MiB
- 难度
- 10
- 标签
- (无)
- 递交数
- 1
- 已通过
- 1
- 上传者