3 条题解
-
0
这里为你提供这道题的标准题解代码,使用 NOIP C++14 竞赛标准。
这道题的核心是一个 字符串模拟题。我们将从符合人类直觉的“最简单暴力拆分版”开始,然后逐渐过渡到不需要额外空间的“最优在线处理版”。
版本一:简单暴力拆分版 (String Split & Validate)
思路: 第一步,先把一整长串按照
,切分成一个个独立的候选密码,存进一个数组(vector<string>);第二步,遍历这个数组,对每一个候选密码应用所有的规则进行检查;第三步,合规的直接输出。#include <iostream> #include <string> #include <vector> using namespace std; // 独立函数:检查单个密码是否合法 bool isValid(const string& s) { // 【关键点1 / 易错点】长度校验必须放在第一步! // 这样做符合短路逻辑,如果长度不对直接返回,避免后续多余的循环计算 if (s.length() < 6 || s.length() > 12) { return false; } // 准备 4 个灯(布尔变量),用来记录字符种类是否出现过 bool has_lower = false; bool has_upper = false; bool has_digit = false; bool has_special = false; // 遍历当前密码的每一个字符 for (char c : s) { if (c >= 'a' && c <= 'z') { has_lower = true; } else if (c >= 'A' && c <= 'Z') { has_upper = true; } else if (c >= '0' && c <= '9') { has_digit = true; } else if (c == '!' || c == '@' || c == '#' || c == '$') { has_special = true; } else { // 【关键点2 / 易错点】一票否决机制! // 题目要求“只能由”以上字符构成,一旦遇到其他字符(比如&, *, 空格),立刻判死刑 return false; } } // 【关键点3】巧用布尔值转化为整数相加 // 大小写和数字至少两种,true 相当于 1,false 相当于 0 int type_count = has_lower + has_upper + has_digit; // 结算:基础类型 >= 2,且必须有特殊字符 if (type_count >= 2 && has_special) { return true; } return false; } int main() { // 优化 C++ 的输入输出流速度 (NOIP 常见技巧) ios::sync_with_stdio(false); cin.tie(nullptr); string input; if (!(cin >> input)) return 0; // 读取一行不含空格的字符串 vector<string> passwords; // 存放切分后的候选密码 string current = ""; // 步骤一:切分字符串 for (char c : input) { if (c == ',') { passwords.push_back(current); current = ""; // 清空,准备存下一个 } else { current += c; } } // 【易错点】最后一段密码后可能没有逗号,循环结束后不要忘了把最后一段加进去 passwords.push_back(current); // 步骤二:遍历验证并输出 for (const string& pwd : passwords) { if (isValid(pwd)) { cout << pwd << "\n"; } } return 0; }版本一复杂度分析思考过程
- 时间复杂度思考:
- 第一阶段切分字符串,我们把每个字符看了一遍,时间是 ( 是总串长度)。
- 第二阶段验证字符串,我们又把
vector里面的每个密码的每个字符看了一遍,时间也是 。 - 总时间复杂度 。由于 ,运算次数极小,绝对可以满分 AC。
- 空间复杂度思考:
- 我们开辟了一个
vector<string>,把原串切开存进去了。最坏情况下,它会占用和原输入字符串等量的内存。 - 总空间复杂度 。
- 我们开辟了一个
- 时间/空间复杂度优化建议:
- 我们真的需要一个
vector来保存拆分好的字符串吗? - 其实不用!检查一个子串只需要它本身的字符信息。我们在遇到
,时,直接拿刚刚拼好的current字符串去验证,验证完直接清空就行了。这样就省去了把所有子串存起来的开销。这就引出了版本二。
- 我们真的需要一个
版本二:最优复杂度在线处理版 (One-Pass Online Processing)
思路: 边读边处理,绝不走回头路。只需要一个临时变量
current来记录当前正在组合的这一段密码。遇到逗号,当场拉去“安检”,安检完立刻打印并清空缓存区。#include <iostream> #include <string> using namespace std; // 验证函数与版本一完全一致,为了不产生重复存储,传递常量引用 const string& bool isValid(const string& s) { if (s.length() < 6 || s.length() > 12) return false; bool has_lower = false, has_upper = false, has_digit = false, has_special = false; for (char c : s) { if (c >= 'a' && c <= 'z') has_lower = true; else if (c >= 'A' && c <= 'Z') has_upper = true; else if (c >= '0' && c <= '9') has_digit = true; else if (c == '!' || c == '@' || c == '#' || c == '$') has_special = true; else return false; } return (has_lower + has_upper + has_digit >= 2) && has_special; } int main() { // 提速 IO ios::sync_with_stdio(false); cin.tie(nullptr); string input; if (!(cin >> input)) return 0; string current = ""; // 临时存放当前正在解析的密码片段 // 边解析边处理 (One-Pass) for (char c : input) { if (c == ',') { // 遇到分隔符,立刻结算当前收集到的 current if (isValid(current)) { cout << current << "\n"; } current = ""; // 【关键点】清空缓存,准备迎接下一段密码 } else { current += c; } } // 【易错点】同样不能忘了最后一段没有逗号结尾的字符串 // 即使原串以逗号结尾,这里 current 就是 ""(空串) // 传入 isValid 后,因为长度 < 6,会自动返回 false,不会导致错误输出 if (isValid(current)) { cout << current << "\n"; } return 0; }版本二复杂度分析思考过程
- 时间复杂度优化: 。只需要遍历原字符串一次(即 次操作)。
isValid函数虽然看起来内部也有一层循环,但每次处理的是片段长度 。所有 的总和加起来依然是 。所以总时间稳定在 。由于去掉了vector动态扩容和反复转移数据的微小开销,常数会更小。 - 空间复杂度优化: ,即 。我们不再存储所有的子串。额外的空间开销仅仅是这个
current变量。由于合法密码最长不超过 ,即便遇到很长的非法密码片段,因为总长最多 ,current占用的空间也是极小的常数级别。在算法竞赛的语境下,这就是常数级 的附加空间。
教练对初学者的叮嘱:
在真实的 NOIP 考场上,对于 的数据规模,版本一和版本二都能拿满分。但是从长远的编程思维来看,版本二的“在线思想”非常重要。当以后数据规模大到几个 GB 无法全部读入内存时,边读边处理是唯一的破局之道。因此强烈建议掌握版本二的思想!
- 时间复杂度思考:
信息
- ID
- 13926
- 时间
- 1000ms
- 内存
- 128MiB
- 难度
- 3
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者