2 条题解
-
0
作为 NOI 教练,制作模拟题数据时,最重要的是覆盖逻辑边界。对于“Word Pattern”这类映射题,数据必须包含:长度不等、多对一映射、一对多映射、以及大规模极限数据。
以下是为您准备的 C++14 数据生成器。它将逻辑解题函数与数据生成逻辑结合,自动产出
1.in/out到10.in/out。一、 数据生成策略
- Test 1-2 (基础验证):手动构造的小规模数据(True/False)。
- Test 3 (长度不匹配):
pattern长度与单词数量不等。 - Test 4 (一对多失败):同一个字符尝试映射到两个不同的单词。
- Test 5 (多对一失败):两个不同的字符尝试映射到同一个单词(这是学生最容易漏掉的逻辑)。
- Test 6 (单字符极限):
pattern和s都只有一个元素。 - Test 7-8 (最大规模匹配):,,字符与单词一一对应,结果为
true。 - Test 9-10 (混合随机):随机生成单词和字符,测试综合鲁棒性。
二、 数据生成器源码 (C++14)
#include <iostream> #include <fstream> #include <vector> #include <string> #include <map> #include <set> #include <sstream> #include <random> #include <chrono> using namespace std; // --- 逻辑求解器:用于生成 .out 文件 --- string solve(string pattern, string s) { vector<string> words; stringstream ss(s); string t; while (ss >> t) words.push_back(t); if (pattern.length() != words.size()) return "false"; map<char, string> p2s; map<string, char> s2p; for (int i = 0; i < (int)pattern.length(); ++i) { char c = pattern[i]; string w = words[i]; if (p2s.count(c) && p2s[c] != w) return "false"; if (s2p.count(w) && s2p[w] != c) return "false"; p2s[c] = w; s2p[w] = c; } return "true"; } // --- 数据生成逻辑 --- mt19937 rng(chrono::steady_clock::now().time_since_epoch().count()); // 生成随机小写单词 string gen_word(int len) { string res = ""; for (int i = 0; i < len; ++i) res += (char)('a' + rng() % 26); return res; } void make_data(int id) { string in_name = to_string(id) + ".in"; string out_name = to_string(id) + ".out"; ofstream fout(in_name); string pattern = ""; string s = ""; vector<string> words; if (id == 1) { // 简单匹配 pattern = "abba"; words = {"dog", "cat", "cat", "dog"}; } else if (id == 2) { // 简单不匹配 pattern = "abba"; words = {"dog", "cat", "cat", "fish"}; } else if (id == 3) { // 长度不等 pattern = "abc"; words = {"dog", "cat"}; } else if (id == 4) { // 一对多失败 pattern = "aaaa"; words = {"dog", "dog", "cat", "dog"}; } else if (id == 5) { // 多对一失败 pattern = "abcd"; words = {"dog", "dog", "dog", "dog"}; } else if (id == 6) { // 最小边界 pattern = "z"; words = {"apple"}; } else if (id <= 8) { // 最大规模 True (id 7,8) int n = 300; vector<string> dict; for(int i=0; i<n; ++i) dict.push_back(gen_word(5)); // 每个单词5字,总长约1800 < 3000 for(int i=0; i<n; ++i) { char c = (char)('a' + (i % 26)); pattern += c; // 为了保证 True,需要确保相同的 c 对应相同的单词 // 这里我们简化处理,让每个位置都不一样 words.push_back(dict[i]); // 注意:如果 i%26 重复,我们需要修正 words 让其符合双射 } // 修正:重新构建符合逻辑的 pattern 和 words pattern = ""; words.clear(); map<char, string> fixed_map; for(int i=0; i<n; ++i) { char c = (char)('a' + (rng() % 26)); pattern += c; if(fixed_map.count(c)) words.push_back(fixed_map[c]); else { string new_w = gen_word(8); fixed_map[c] = new_w; words.push_back(new_w); } } } else { // 混合随机 (id 9,10) int n = 150 + rng() % 150; for (int i = 0; i < n; ++i) pattern += (char)('a' + rng() % 26); for (int i = 0; i < n; ++i) words.push_back(gen_word(4)); } // 将 words 组合成字符串 s for (int i = 0; i < (int)words.size(); ++i) { s += words[i]; if (i != (int)words.size() - 1) s += " "; } // 限制 s 长度不超过 3000 (针对随机生成的简单截断) if (s.length() > 3000) s = s.substr(0, 3000); fout << pattern << endl; fout << s << endl; fout.close(); // 生成答案 ofstream fans(out_name); fans << solve(pattern, s) << endl; fans.close(); cout << "Test Case " << id << " Generated." << endl; } int main() { for (int i = 1; i <= 10; ++i) { make_data(i); } return 0; }
三、 优化与防错思考
- 分词性能:
数据生成器中使用了
stringstream进行分词。虽然在极大规模下(如 )它可能稍慢,但在本题 的规模下,其处理速度在毫秒级,不会导致生成超时。 - 避免除零:
代码中唯一涉及取模的操作是
rng() % 26和rng() % n。由于除数均为固定正整数或经过范围限定的正数,不存在除以 0 的风险。 - 栈溢出防御: 本题的逻辑(双射判定)是线性的,不涉及任何深层递归或图论遍历,因此不存在爆栈风险。
- IO 缓冲区说明:
getline(cin >> ws, s)是竞赛中读取pattern之后读取带空格字符串的常用技巧。ws算子会自动跳过cin >> pattern留在缓冲区里的回车符。 - 数据有效性:
在
id=7,8的生成逻辑中,我特别注意了如果pattern中出现了相同的字符,对应的单词必须相同才能生成true的样例。这保证了测试数据中既有高质量的true也有高质量的false。
四、 如何在 OJ 使用
- 编译运行此生成器,得到
1.in/out ~ 10.in/out。 - 将
pattern长度设定为 ,s长度设定为 的上限。 - 测评时,注意 C++ 的
cin >> pattern后面紧跟getline时,务必提醒选手处理掉中间的换行符(这是本题最常见的选手失分点)。
-
0
这道题在字符串模拟中非常经典,主要考察对**双射(One-to-One Correspondence)**逻辑的严谨实现。在 NOI 竞赛中,这类题目通常作为签到题或初级模拟题出现。
一、 Word Pattern 标准题解 (C++14)
#include <iostream> #include <string> #include <vector> #include <sstream> #include <map> using namespace std; /** * 判定函数:判定 pattern 与 s 是否满足双射关系 */ bool check_word_pattern(const string& pattern, const string& s) { vector<string> words; stringstream ss(s); string temp; // 1. 将字符串 s 按空格切分为单词序列 // 易错点:如果 s 结尾有冗余空格,需确保切分逻辑的鲁棒性 while (ss >> temp) { words.push_back(temp); } // 2. 数量校验 // 易错点:如果 pattern 长度与单词数量不等,必然不符合规律 if (pattern.length() != words.size()) { return false; } // 3. 建立双向映射 // p2s: 字符 -> 单词 // s2p: 单词 -> 字符 map<char, string> p2s; map<string, char> s2p; for (int i = 0; i < (int)pattern.length(); ++i) { char ch = pattern[i]; string word = words[i]; // 检查 正向映射:字符 ch 是否已经映射到了别的单词 if (p2s.count(ch)) { if (p2s[ch] != word) return false; } else { p2s[ch] = word; } // 检查 反向映射:单词 word 是否已经由别的字符映射过来 // 易错点:漏掉反向判定会导致 "abba" -> "dog dog dog dog" 判定为 true if (s2p.count(word)) { if (s2p[word] != ch) return false; } else { s2p[word] = ch; } } return true; } int main() { // 优化 I/O 效率 ios::sync_with_stdio(false); cin.tie(0); string pattern, s; // 读取 pattern if (!(cin >> pattern)) return 0; // 易错点:cin >> pattern 后缓冲区留有换行符,需使用 ws 跳过空白字符后再 getline // 或者先 getchar() 消耗掉换行符 getline(cin >> ws, s); if (check_word_pattern(pattern, s)) { cout << "true" << endl; } else { cout << "false" << endl; } return 0; }
二、 复杂度分析思考过程
1. 时间复杂度分析
- 分词阶段:
stringstream遍历一次字符串 ,复杂度为 ,其中 为 的长度。 - 匹配阶段:遍历
pattern,长度为 。- 在
map中进行查找和插入操作,复杂度为 ,其中 是去重后的单词/字符数, 是字符串的平均长度。
- 在
- 总体复杂度:。
- 在本题规模下(),由于 ,整体复杂度接近 。这在 1s 限制内绰绰有余(哪怕 也能跑过)。
2. 空间复杂度分析
- 存储开销:
vector<string>存储了所有的单词,空间为 。 - 映射表开销:两个
map存储了字符与单词的对应关系,最坏情况下空间也为 。 - 结论:总空间复杂度 。
三、 时间复杂度优化建议
-
哈希表优化: 将
std::map替换为std::unordered_map。std::map底层是红黑树(),而unordered_map是哈希表(平均 )。对于大规模数据,哈希表能显著减少常数项开销。 -
空间换时间(针对字符映射): 由于
pattern仅包含小写字母,可以使用一个固定长度为 26 的数组string char_to_word[26]来替代正向映射map。string p2s[26]; bool used_p[26] = {false}; // ... 在循环中 ... int idx = ch - 'a'; if(used_p[idx]) { if(p2s[idx] != word) return false; } else { p2s[idx] = word; used_p[idx] = true; } -
单次遍历分词: 不使用
stringstream或vector<string>,而是直接在遍历pattern的过程中,利用双指针动态地从 中截取单词。这可以节省存储单词列表所需的内存空间。
四、 读题理解关键词总结
在 NOI 字符串类题目中,看到以下表述需立即警觉:
- “遵循相同的规律”:通常指结构上的同构或映射关系。
- “每个...对应一个...”:这是映射的定义。一定要注意是单向映射还是双向映射(双射)。
- 如果题目说“A 对应 B”,不代表“B 只能对应 A”。
- 如果题目说“一一对应”,则必须写双向哈希比对。
- “非空单词”与“单个空格”:这是模拟分词的边界条件。如果题目说“单词间可能有多个空格”,则不能使用简单的固定偏移量寻找单词,必须用
while(s[i] == ' ') i++;来跳过空白。 - 数据规模 (300, 3000):这么小的规模暗示你可以使用常数略大但更安全的
std::map或std::string操作,而不需要过早进行复杂的位运算优化。
- 分词阶段:
- 1
信息
- ID
- 19387
- 时间
- 1000ms
- 内存
- 128MiB
- 难度
- 10
- 标签
- (无)
- 递交数
- 1
- 已通过
- 1
- 上传者