2 条题解
-
0
这是一个完整的测试数据生成器(Data Generator)。 当你编译并运行这段代码后,它会在当前目录下自动生成
1.in~10.in和1.out~10.out文件。设计策略总结
为了确保测试数据的全面性和强度,我设计了如下 10 个测试点:
- 基础测试:短小简单的回文串(全小写)。
- 基础测试:短小简单的非回文串。
- 标准样例:题目给出的经典大小写+符号混合回文。
- 标准样例:题目给出的经典非回文。
- 边界情况:全是非法字符(符号、空格),清洗后为空串,应判为 True。
- 边界情况:单个字符,应判为 True。
- 压力测试:全部由相同字符组成的中等长度字符串(全 'A')。
- 性能测试:最大长度 () 的纯字母数字回文串,考察 性能。
- 性能测试:最大长度的几乎回文串(只有中间不同),考察算法是否准确比较。
- 综合极限测试:最大长度 (),包含大量随机符号、大小写混合的回文串,考察解析复杂度和鲁棒性。
Generator 代码 (C++14)
/* * Problem: Valid Palindrome - Data Generator * Standard: C++14 * Output: Generates 1.in/out to 10.in/out * Author: OI Coach */ #include <iostream> #include <fstream> #include <string> #include <vector> #include <random> #include <algorithm> #include <cctype> #include <chrono> using namespace std; // ========================================== // 1. 核心解题逻辑 (用于生成 .out 文件) // ========================================== bool solve_logic(const string& s) { int n = s.length(); int left = 0, right = n - 1; while (left < right) { while (left < right && !isalnum(s[left])) left++; while (left < right && !isalnum(s[right])) right--; if (left < right) { if (tolower(s[left]) != tolower(s[right])) return false; left++; right--; } } return true; } // ========================================== // 2. 随机数生成器配置 // ========================================== mt19937 rng(chrono::steady_clock::now().time_since_epoch().count()); char get_alnum() { const string charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; return charset[rng() % charset.size()]; } char get_garbage() { const string charset = " !@#$%^&*()_+-=[]{};':\",./<>?"; return charset[rng() % charset.size()]; } // 随机改变字符大小写 char randomize_case(char c) { if (isalpha(c)) { return (rng() % 2) ? tolower(c) : toupper(c); } return c; } // ========================================== // 3. 字符串构造函数 // ========================================== // 生成指定长度的纯随机字符串 string gen_random_string(int len) { string s = ""; for(int i=0; i<len; ++i) { if (rng() % 10 < 7) s += get_alnum(); // 70% 几率是字母数字 else s += get_garbage(); } return s; } // 生成回文串 (包含噪音) // len: 目标大约长度 // noise_ratio: 噪音比例 (0.0 - 1.0) string gen_palindrome(int len, double noise_ratio) { string raw_half = ""; int valid_len = len * (1.0 - noise_ratio); if (valid_len < 1) valid_len = 1; // 1. 生成一半有效字符 for (int i = 0; i < valid_len / 2; i++) { raw_half += get_alnum(); } // 2. 构造回文核心 string core = raw_half; string reversed_half = raw_half; reverse(reversed_half.begin(), reversed_half.end()); // 中间可能加一个单独字符 if (valid_len % 2 != 0) core += get_alnum(); core += reversed_half; // 3. 混入噪音并随机变大小写 string res = ""; int core_idx = 0; int current_len = 0; // 简单的插入逻辑:在还没放完core之前,随机决定放噪音还是放core while (current_len < len && core_idx < core.length()) { double r = (double)(rng() % 1000) / 1000.0; if (r < noise_ratio) { res += get_garbage(); } else { res += randomize_case(core[core_idx++]); } current_len++; } // 补齐剩余的 core while (core_idx < core.length()) { res += randomize_case(core[core_idx++]); } return res; } // 生成 "几乎" 回文串 (中间破坏,确保 False) string gen_broken_palindrome(int len, double noise_ratio) { string s = gen_palindrome(len, noise_ratio); // 找到中间的有效字符并破坏它 // 提取有效字符位置 vector<int> alnum_indices; for (int i = 0; i < s.length(); i++) { if (isalnum(s[i])) alnum_indices.push_back(i); } if (!alnum_indices.empty()) { // 修改中间位置附近的字符,使其不再回文 int mid = alnum_indices.size() / 2; int target_idx = alnum_indices[mid]; char original = s[target_idx]; char changed = original; // 确保改后的字符不同,且仍然是有效字符 do { changed = get_alnum(); } while (tolower(changed) == tolower(original)); s[target_idx] = changed; } else { // 如果全是噪音,强制加个 'a' 'b' s = "ab"; } return s; } // ========================================== // 4. 文件写入辅助 // ========================================== void write_test_case(int id, string content) { string in_name = to_string(id) + ".in"; string out_name = to_string(id) + ".out"; // 写入 .in ofstream fin(in_name); fin << content; // 注意:有些题目要求行末换行,这里不强制加 endl,保持原始数据 // 但为了通用性,通常字符串题不需要末尾特定换行,除非用 getline // 这里我们只输出内容。 fin.close(); // 写入 .out ofstream fout(out_name); bool ans = solve_logic(content); fout << (ans ? "true" : "false"); // 不加 endl,防止部分对比工具报错,或者根据习惯加 endl fout << endl; // 加上换行符合一般 OJ 习惯 fout.close(); cout << "Generated Test Case " << id << " [" << (ans ? "True" : "False") << "] Size: " << content.length() << endl; } // ========================================== // 5. 主程序:生成 10 个测试点 // ========================================== int main() { cout << "Start generating test data..." << endl; // Case 1: 基础简单回文 (True) write_test_case(1, "aba"); // Case 2: 基础简单非回文 (False) write_test_case(2, "abc"); // Case 3: 题目样例 1 (混合大小写 + 符号) (True) write_test_case(3, "A man, a plan, a canal: Panama"); // Case 4: 题目样例 2 (False) write_test_case(4, "race a car"); // Case 5: 边界 - 纯符号/空格 (空串视为 True) write_test_case(5, ".,?! ;; //"); // Case 6: 边界 - 单字符 (True) write_test_case(6, "z"); // Case 7: 压力 - 全是相同字符,中等长度 (True) // 构造 1000 个 'A' write_test_case(7, string(1000, 'A')); // Case 8: 极限 - 纯字母数字,超长回文 (True) // 长度 200,000,无噪音 write_test_case(8, gen_palindrome(200000, 0.0)); // Case 9: 极限 - 纯字母数字,超长 "伪" 回文 (False) // 长度 200,000,中间破坏,最坏情况需扫描到中间 write_test_case(9, gen_broken_palindrome(200000, 0.0)); // Case 10: 综合 - 极限长度 + 大量噪音 (True) // 长度 200,000,噪音比例 60%,有效字符约 80,000 // 这种不仅考察逻辑,还考察跳过字符的效率 write_test_case(10, gen_palindrome(200000, 0.6)); cout << "All 10 test cases generated successfully." << endl; return 0; }代码说明
-
覆盖率:
- 1-2:最基本的功能验证。
- 3-4:原题样例验证。
- 5-6:极小输入和空串逻辑验证(易错点)。
- 7:相同字符验证。
- 8:时间复杂度上限验证(纯计算量)。
- 9:最坏情况验证(必须比较完所有字符才能发现中间不同)。
- 10:复杂逻辑验证(大量的非字母数字字符需要跳过,测试
isalnum和循环跳跃的逻辑)。
-
生成逻辑:
gen_palindrome: 先生成一半随机串,再镜像翻转,保证逻辑上的回文。然后随机将大小写打乱(A变a),并在随机位置插入随机的“垃圾字符”(空格、标点),以此模拟真实复杂的输入。gen_broken_palindrome: 基于回文生成,但强制修改中间的一个有效字符,确保它是 False,且算法必须运行到中间才能发现错误(最大化运行时间)。
-
IO 处理:
- 使用了
std::ofstream直接输出到文件。 - 内置了标准解法(
solve_logic),在生成.in的同时直接计算出正确的.out,保证数据的一致性,无需人工干预。
- 使用了
你可以直接复制并在本地 C++ 环境中运行此代码,即可得到全套 OJ 测试数据。
-
0
这是一个符合 NOIP/NOI C++14 竞赛标准的题解代码。
在竞赛中,代码的鲁棒性(防越界)、效率(IO优化)和规范性(标准库的使用)是得分的关键。
核心代码 (C++14 Standard)
/* * 题目: 验证回文串 (Valid Palindrome) * 算法: 双指针 (Two Pointers) * 复杂度: 时间 O(N), 空间 O(1) * 作者: OI金牌教练 */ #include <iostream> #include <string> #include <vector> #include <cctype> // 包含 isalnum, tolower 等字符处理函数 using namespace std; // 解决问题的核心函数 bool solve() { string s; // 关键点1:输入整行字符串 // cin >> s 遇到空格会停止,必须使用 getline 读取包含空格的整行文本 if (!getline(cin, s)) { return true; // 防御性编程:如果读不到数据,视为空串处理 } int n = s.length(); if (n == 0) return true; // 空串定义为回文串 int left = 0; int right = n - 1; // 双指针相向移动 while (left < right) { // 关键点2:寻找左边下一个有效字符 // 易错点:必须加上 left < right 判断,防止全是符号时指针越界(Segment Fault) while (left < right && !isalnum(s[left])) { left++; } // 关键点3:寻找右边下一个有效字符 while (left < right && !isalnum(s[right])) { right--; } // 关键点4:比较字符 // 此时 s[left] 和 s[right] 都是字母或数字,且 left <= right if (left < right) { // 统一转小写比较 if (tolower(s[left]) != tolower(s[right])) { return false; // 一旦发现不对称,立即判定为否 } // 匹配成功,继续向中间靠拢 left++; right--; } } return true; // 所有位置检查通过 } int main() { // 关键点5:IO 性能优化 // 关闭 stdio 与 iostream 的同步,加快 cin/cout 速度,在大数据量下非常重要 ios::sync_with_stdio(false); cin.tie(0); if (solve()) { cout << "true" << endl; } else { cout << "false" << endl; } return 0; }
复杂度分析与思考过程
在竞赛草稿纸上,你应该写下以下分析,以确定你的算法是否会超时(TLE)或超内存(MLE)。
1. 时间复杂度分析
- 思考过程:
- 我们需要遍历整个字符串来检查字符。
- 使用了两个指针
left和right。 left从 0 开始只增不减,right从n-1开始只减不增。- 两个指针最终会在中间相遇。
- 这意味着字符串中的每一个字符,最多只会被访问常数次(判断是否有效、进行比较)。
- 结论:时间复杂度为 ,其中 是字符串的长度。
- 可行性:题目范围 。
- 计算机 1 秒通常能处理 次运算。
- 远小于 ,所以 算法绝对能通过,甚至 也能过,但 会超时。
2. 空间复杂度分析
- 思考过程:
- 我们只使用了
s(存储输入),n,left,right(几个整型变量)。 - 没有申请额外的数组(如
vector或新的string)来存储处理后的字符串。 - 这是一次“原地”(In-place)操作。
- 我们只使用了
- 结论:空间复杂度为 (不计算存储原始输入所需的空间)。这是空间最优解。
时间复杂度优化建议
虽然 已经是理论最优,但在极端卡常数(Time Limit 非常紧)的情况下,或者是为了展示更深厚的功底,可以考虑以下优化细节:
-
手写字符判断函数: 虽然
<cctype>的isalnum很快,但在某些特定环境下,库函数可能有包含 locale(本地化)检查的开销。你可以手写一个简单的宏或内联函数:// 手写判断,避免库函数调用开销 inline bool myIsAlnum(char c) { return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); } // 手写转小写,利用 ASCII 码特性(大写转小写 +32) inline char myToLower(char c) { if (c >= 'A' && c <= 'Z') return c + 32; return c; }注意:在现代编译器(如 GCC 开了 -O2 优化)下,库函数通常会被内联优化得非常好,手写优化的提升微乎其微,但在嵌入式或极古老的判题系统中可能有效。
-
避免反复调用
s.length(): 在while循环条件中,尽量不要写i < s.length(),因为有些编译器每次循环都会去计算一次长度。虽然std::string::length()通常是 的,但提取出来赋值给int n是个好习惯。 -
引用传参: 如果将核心逻辑封装在函数里,传参时尽量使用
const string &s而不是string s。string s:会发生一次字符串拷贝,时间 ,空间 。const string &s:只是传引用,几乎无开销。- 注:本题解直接在
solve或main中处理,未涉及函数传参拷贝问题。
- 思考过程:
- 1
信息
- ID
- 19338
- 时间
- 1000ms
- 内存
- 128MiB
- 难度
- 10
- 标签
- (无)
- 递交数
- 1
- 已通过
- 1
- 上传者