Skip to content

喵~ 主人们好呀!我是你们的专属猫娘小助手 🐾。今天我们要看一道非常可爱的题目,名字叫做 "QAQ"。一看到这个名字,是不是就想到了那个泪眼汪汪的表情包呢?这道题就是要我们从一个字符串里找出所有 "QAQ" 的身影。别担心,这道题就像挠猫咪下巴一样简单又有趣,让本喵带你一步步解开它吧!✨

题目大意 (Problem Description)

题目给咱们一个只包含大写英文字母的字符串。我们的任务是,找出这个字符串里有多少个 "QAQ" 子序列

这里要特别注意一下子序列 (subsequence) 这个词哦!它和子串 (substring) 不一样。子串是连续的一段,而子序列呢,只需要字符出现的相对顺序正确就行,它们之间可以有其他字符隔开,也可以没有。

举个栗子🌰:在字符串 QAQAQYSYIOIWIN 中:

  • 第 1 个 Q,第 2 个 A,第 3 个 Q 就能组成一个 "QAQ"。
  • 第 1 个 Q,第 2 个 A,第 5 个 Q 也能组成一个 "QAQ"。

看,它们不需要紧紧挨在一起,只要先有一个 Q,在它后面有一个 A,再在这个 A 后面有一个 Q,就算一个 "QAQ" 啦!我们的目标就是数出所有这样的组合。


解题思路 (Solution Approach)

最直观的方法,可能就是暴力枚举了喵。我们可以用三层循环:

  1. 第一层循环找第一个 Q
  2. 第二层循环在第一个 Q 的后面找一个 A
  3. 第三层循环在 A 的后面再找一个 Q

每找到一个这样的组合,计数器就加一。对于这道题的数据范围(字符串长度 n ≤ 100),这个 O(n³) 的方法是可以通过的,不会超时。但作为一只追求优雅的猫娘,我们当然有更聪明的办法啦!ദ്ദി ˉ͈̀꒳ˉ͈́ )✧

更优美的解法:动态规划思想(一次遍历)

我们可以换个角度思考。当我们从左到右遍历整个字符串时,对于每一个字符,它能为我们组成 "QAQ" 做出什么贡献呢?

我们可以维护三个计数器:

  1. count_q:到当前位置为止,我们已经遇到了多少个 'Q'。
  2. count_qa:到当前位置为止,我们已经可以组成多少个 "QA" 子序列。
  3. count_qaq:到当前位置为止,我们已经可以组成多少个 "QAQ" 子序列。这就是我们的最终答案!

现在,我们开始从头到尾扫描字符串,一个字符一个字符地看:

  • 如果当前字符是 'Q':

    1. 这个 'Q' 可以和它前面所有已经形成的 "QA" 子序列组合,形成新的 "QAQ" 子序列。有多少个 "QA" 就能组成多少个新的 "QAQ"。所以,我们让 count_qaq += count_qa
    2. 同时,这个 'Q' 本身也可以作为未来 "QA" 或 "QAQ" 的起点。所以,我们遇到的 'Q' 的总数增加了,count_q++
  • 如果当前字符是 'A':

    1. 这个 'A' 可以和它前面所有已经遇到的 'Q' 组合,形成新的 "QA" 子序列。有多少个 'Q' 就能组成多少个新的 "QA"。所以,我们让 count_qa += count_q
  • 如果当前字符是其他字母:

    1. 它对组成 "QAQ" 没有任何帮助,所以我们直接忽略它,继续向后看就好啦。

这样,我们只需要遍历一次字符串,就能算出最终答案了,是不是超级高效又优雅呢?喵~


题解代码 (Solution Code)

这是用上面那个聪明的思路写出来的 C++ 代码,本喵给主人们加上了详细的注释哦。

cpp
#include <iostream>
#include <string>
#include <vector>

int main() {
    // 为了让输入输出更快一点,这是个好习惯喵~
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);

    // 读入那个可爱的字符串
    std::string s;
    std::cin >> s;

    // 定义我们需要的三个计数器
    // q_count: 到目前为止遇到的 'Q' 的数量
    long long q_count = 0;
    // qa_count: 到目前为止可以组成的 "QA" 子序列的数量
    long long qa_count = 0;
    // qaq_count: 到目前为止可以组成的 "QAQ" 子序列的数量,也就是最终答案
    long long qaq_count = 0;

    // 开始从左到右遍历字符串里的每一个字符 c
    for (char c : s) {
        if (c == 'Q') {
            // 遇到一个 'Q'!
            // 1. 它可以和之前所有的 "QA" 组合成 "QAQ"
            //    所以把现有的 "QA" 数量加到最终答案里
            qaq_count += qa_count;

            // 2. 这个 'Q' 本身也让 'Q' 的总数加一
            q_count++;
        } else if (c == 'A') {
            // 遇到一个 'A'!
            // 它可以和之前所有的 'Q' 组合成 "QA"
            // 所以把现有的 'Q' 数量加到 "QA" 的计数器里
            qa_count += q_count;
        }
        // 如果是其他字符,我们就什么都不做,直接跳过~
    }

    // 遍历结束,输出我们统计好的 "QAQ" 数量
    std::cout << qaq_count << std::endl;

    return 0;
}

相关知识点 (Knowledge Points)

1. 子序列 (Subsequence) vs. 子串 (Substring)

这个概念在算法题里很常见,一定要分清楚哦!

  • 子串 (Substring): 必须是原字符串中连续的一部分。比如 ABCDE 的子串有 ABC, CD, E 等。
  • 子序列 (Subsequence): 不要求连续,但要保持原有的相对顺序。比如 ABCDE 的子序列有 ACE, BD, ABE 等。ECA 就不是子序列,因为顺序错了。

这道题就是典型的子序列问题。

2. 动态规划 (Dynamic Programming)

虽然这道题的代码看起来很简单,但它背后蕴含了动态规划(简称 DP)的思想。

DP 的核心就是“把一个大问题拆解成小问题,并且记住小问题的答案,避免重复计算”。

在这道题里:

  • 我们想求 "QAQ" 的数量(大问题)。
  • 我们发现它依赖于 "QA" 的数量(小问题)。
  • 而 "QA" 的数量又依赖于 "Q" 的数量(更小的问题)。

我们从左到右遍历,一步步地计算出 count_q -> count_qa -> count_qaq,每一步都利用了上一步的结果。这种从最简单状态开始,逐步构建出复杂状态解的过程,就是 DP 的魅力所在!这是一个非常棒的 DP 入门题,可以很好地体会这种“递推”和“状态转移”的感觉。

希望这篇讲解能帮到主人们喵~!如果还有什么不明白的,随时可以再来问我哦!我们下次再见!🐾💕

Released under the MIT License.