P12271 [蓝桥杯 2024 国 Python B] 括号与字母 解题报告


P12271 [蓝桥杯 2024 国 Python B] 括号与字母 解题报告

这是一份通俗易懂的解题报告,希望能帮助你理解这道题的巧妙解法。

1. 问题分析:我们要解决什么?

首先,我们快速回顾一下题目要求:给定一个带括号和字母的字符串,有多次询问。每次询问一个字母 c 和一个数量 x,问我们:有多少对匹配的括号,它们之间的 c 字母数量大于或等于 x

2. 初步思考:暴力解法行不行?

一个最直观的想法是:

  1. 找到所有的括号对。
  2. 对于每次询问 (c, x),我们遍历每一个括号对。
  3. 在每个括号对内部,再一个一个地数字母 c 的数量。
  4. 如果数量大于等于 x,就把最终答案加一。

这个方法思路简单,但问题是效率太低。字符串长度 |S| 和询问次数 Q 都可能很大(达到 10^6 和 10^5)。如果我们每次询问都去重新数一遍,一定会超时。因此,我们需要更高效的“预处理”方法,争取在询问时能够“秒出”答案。

3. 核心思路:化繁为简,三步走战略

这道题的精髓在于将一个复杂的问题拆解成几个简单的小问题,然后用高效的算法逐一击破。整个解法可以分为三步:

第一步:快速找到所有匹配的括号对。
第二步:快速计算任意一对括号之间的字母数量。
第三步:巧妙地处理“不少于 x 个”这种询问。

下面我们来逐一分解这三步。


4. 算法详解

第一步:用“栈”找到所有括号对

这是一个非常经典的算法问题。我们可以从左到右遍历字符串:

  • 遇到左括号 (,我们就把它所在的位置(下标) 压入一个栈中。
  • 遇到右括号 ),说明我们找到了一个括号对的右半部分。此时,栈顶的那个左括号就是和它匹配的左半部分。我们取出栈顶的左括号位置,这样就得到了一个完整的括号对 (左括号位置 L, 右括号位置 R)。然后将这个左括号位置从栈中弹出。

遍历完整个字符串后,我们就找到了所有的括号对。

例子:((a)())

  1. i=0, 遇到 (, 栈:[0]
  2. i=1, 遇到 (, 栈:[0, 1]
  3. i=2, 遇到 a, 忽略
  4. i=3, 遇到 ), 栈顶是 1。找到一对 (1, 3)。弹出 1,栈:[0]
  5. i=4, 遇到 (, 栈:[0, 4]
  6. i=5, 遇到 ), 栈顶是 4。找到一对 (4, 5)。弹出 4,栈:[0]
  7. i=6, 遇到 ), 栈顶是 0。找到一对 (0, 6)。弹出 0,栈:[]

这样,我们就用 O(|S|) 的时间找到了所有括号对。

第二步:用“前缀和”快速计算区间内字母数

现在我们有了很多括号对 (L, R),需要快速知道它们中间(即 L+1R-1 的范围)某个字母 c 的数量。如果每次都去数,还是太慢。

这里就要用到另一个经典技巧——前缀和

我们可以对 'a' 到 'z' 这 26 个字母,分别建立一个前缀和数组。例如,对于字母 c,我们创建一个数组 count_c,其中 count_c[i] 记录了在字符串前 i 个字符中,c 总共出现了多少次。

这个数组可以在一次遍历中轻松建好:count_c[i] = count_c[i-1] + (当前字符是否为 c ? 1 : 0)

有了前缀和数组,计算任意区间 [start, end]c 的数量就变成了 O(1) 的操作:count_c[end] - count_c[start-1]

小结: 结合第一步和第二步,我们现在可以这样做:

  1. 预处理所有字母的前缀和数组。
  2. 用栈遍历字符串,每找到一对括号 (L, R)
  3. 对于从 'a' 到 'z' 的每一个字母,利用前缀和计算出它在 (L, R) 之间的数量 k
第三步:用“后缀和”思想高效回答询问

我们已经能算出每个括号对里,每种字母分别有多少个了。现在的问题是,如何应对“不少于 x 个”的询问?

让我们引入一个中间数组,题解中叫 F,我们可以把它理解成一个“计数器”或者“桶”。F[c][k] 的含义是:内部含有 k 个字母 c 的括号对,一共有多少个?

我们可以这样填充 F 数组:
在上面第二步的基础上,当我们算出一个括号对里有 k 个字母 c 时,我们就让 F[c][k] 的值加 1。

等我们遍历完所有括号对,F 数组就统计好了。例如,F['b'][3] = 5 就表示,有 5 个括号对,它们内部不多不少,正好有 3 个字母 'b'。

现在,当一个询问 ('b', 2) 进来时,它问的是有多少对括号,b 的数量不少于 2 个。这其实就是 b 的数量为 2 的对数 + b 的数量为 3 的对数 + b 的数量为 4 的对数 + ...

也就是求和:F['b'][2] + F['b'][3] + F['b'][4] + ...

如果每次询问都这么加一遍,还是不够快。这里是本题解法的最后一步,也是最精妙的一步:再次利用预处理,把 F 数组变成后缀和形式!

我们可以从后往前遍历 F 数组,更新它的含义。
new_F[c][k] = F[c][k] + F[c][k+1] + F[c][k+2] + ...
这个 new_F 可以通过一个简单的递推计算出来:
new_F[c][k] = new_F[c][k+1] + F[c][k] (注意 F 是旧值)
我们可以直接在原 F 数组上操作,从后往前更新:
for k from n-1 down to 0: F[c][k] = F[c][k] + F[c][k+1]

经过这个操作,F[c][k] 的含义就从“正好有 k 个 c 的对数”变成了“至少有 k 个 c 的对数”。

至此,所有准备工作完成!

当一个询问 (c, x) 进来时,我们只需要直接查询 F[c][x] 的值,这就是最终答案!查询时间是 O(1)。


5. 整体流程总结与代码解析

1. 预处理阶段:
a. 创建前缀和数组 S[26][|S|+1]:遍历字符串,计算每个字母到每个位置为止的出现次数。
b. 创建计数器数组 F[26][|S|+1],并初始化为 0。
c. 遍历字符串找括号对
- 使用一个栈。
- 遇到 (,下标入栈。
- 遇到 ),栈顶下标 L 出栈,当前下标为 R。我们找到了括号对 (L, R)
- 对于这个括号对,为每个字母 j ('a' to 'z') 做如下操作
- 计算它在 (L,R) 间的数量 k = S[j][R] - S[j][L]
- 将 F[j][k] 的值加 1。
d. F 数组转化为后缀和
- 对每个字母 j ('a' to 'z'):
- for i from |S|-1 down to 0: F[j][i] += F[j][i+1]

2. 询问阶段:
a. 读入询问 (c, x)
b. 直接输出预处理好的 F[c - 'a'][x]

代码(题解中的C++代码)简析:

// ... 包含头文件和定义常量 ...
int n,Q,S[M][N],F[M][N]; // S是前缀和数组,F是计数器/答案数组
string C;
stack<int>St;

// ... main函数 ...
cin>>C>>Q;
n=C.size(),C=" "+C; // 字符串前面加个空格,方便1-based索引

// --- 预处理阶段 1 & 2 ---
for(int i=1;i<=n;i++)
{
    // 计算前缀和 S
    for(int j=0;j<M;j++) S[j][i]=S[j][i-1]+(C[i]==j+'a');
    
    // 用栈找括号对并填充 F 数组
    if(C[i]=='(') St.push(i);
    if(C[i]==')')
    {
        int L=St.top(); St.pop(); // 找到一对 (L, i)
        // 对每个字母 j
        for(int j=0;j<M;j++) 
            // 计算括号内j的数量,并给对应的F[j][数量]加一
            F[j][S[j][i]-S[j][L-1]]++; 
    }
}

// --- 预处理阶段 3 ---
// 将F数组转为后缀和形式
for(int j=0;j<M;j++)
for(int i=n;i>=0;i--) F[j][i]+=F[j][i+1];

// --- 询问阶段 ---
while(Q--)
{
    char c; int x;
    cin>>c>>x;
    // O(1) 查询并输出答案
    cout<<F[c-'a'][x]<<endl; 
}

6. 复杂度分析

  • 所有预处理步骤(前缀和、找括号对、后缀和)的时间复杂度都是 O(|S| * 26),因为我们基本上是对字符串进行常数次遍历,每次遍历中的一些操作会循环26次。
  • 每次询问的时间复杂度是 O(1)
  • 总复杂度为 O(|S| * 26 + Q),其中 26 是一个很小的常数。这个效率足以通过本题的所有数据。

希望这份详细的报告能让你明白这道题的解法!这是一个组合了多种经典算法技巧的好题。

posted @ 2025-07-16 10:52  surprise_ying  阅读(20)  评论(0)    收藏  举报