3 条题解

  • 0
    @ 2026-6-9 14:16:49

    这里为你提供这道题的标准题解代码,使用 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;
    }
    

    版本一复杂度分析思考过程

    • 时间复杂度思考:
      • 第一阶段切分字符串,我们把每个字符看了一遍,时间是 O(N)O(N)NN 是总串长度)。
      • 第二阶段验证字符串,我们又把 vector 里面的每个密码的每个字符看了一遍,时间也是 O(N)O(N)
      • 总时间复杂度 O(N)+O(N)=O(N)O(N) + O(N) = O(N)。由于 N100N \le 100,运算次数极小,绝对可以满分 AC。
    • 空间复杂度思考:
      • 我们开辟了一个 vector<string>,把原串切开存进去了。最坏情况下,它会占用和原输入字符串等量的内存。
      • 总空间复杂度 O(N)O(N)
    • 时间/空间复杂度优化建议:
      • 我们真的需要一个 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;
    }
    

    版本二复杂度分析思考过程

    • 时间复杂度优化: O(N)O(N)。只需要遍历原字符串一次(即 NN 次操作)。isValid 函数虽然看起来内部也有一层循环,但每次处理的是片段长度 LL。所有 LL 的总和加起来依然是 NN。所以总时间稳定在 O(N)O(N)。由于去掉了 vector 动态扩容和反复转移数据的微小开销,常数会更小。
    • 空间复杂度优化: O(Lmax)O(L_{max}),即 O(1)O(1)。我们不再存储所有的子串。额外的空间开销仅仅是这个 current 变量。由于合法密码最长不超过 1212,即便遇到很长的非法密码片段,因为总长最多 100100current 占用的空间也是极小的常数级别。在算法竞赛的语境下,这就是常数级 O(1)O(1) 的附加空间。

    教练对初学者的叮嘱:

    在真实的 NOIP 考场上,对于 N100N \le 100 的数据规模,版本一和版本二都能拿满分。但是从长远的编程思维来看,版本二的“在线思想”非常重要。当以后数据规模大到几个 GB 无法全部读入内存时,边读边处理是唯一的破局之道。因此强烈建议掌握版本二的思想!

    信息

    ID
    13926
    时间
    1000ms
    内存
    128MiB
    难度
    3
    标签
    递交数
    1
    已通过
    1
    上传者