Files
python/TangDou/JiShuanKe/2023-03-10/T3668.md
HuangHai 1f397eca87 'commit'
2025-08-30 18:35:01 +08:00

202 lines
8.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## [计蒜客 $T3668$ $Eye$ $of$ $the$ $Storm$](https://www.jisuanke.com/problem/T3668)
### 一、题目描述
云浅来到了风暴的中心。这里漂浮着一个长为 $n$ 的,由小写字母组成的字符串 $S$。**字符串的下标从 $1$ 开始。**
想要逃出风暴,就需要回答一些询问。
每次询问会给出一对正整数 $l,r$ 和一个字符串 $T$,云浅需要回答 $S_{l\cdots r}$ 这段子串内有多少个子序列是 $T$。**这里保证 $T$ 的长度为 $2$。**
形式化地,你需要求出有多少对 $(i,j)$ 满足 $l\le i<j\le r$,使得 $S_i=T_1,S_j=T_2$。
现在云浅预测出了风暴在接下来 $q$ 个时刻内的询问,你需要帮她求出每个询问的答案。
### 输入格式
第一行两个正整数 $n,q$。
第二行一个长为 $n$ 的字符串 $S$。
接下来 $q$ 行,每行会给出两个正整数 $l,r$ 和一个字符串 $T$,表示云浅需要回答的询问。保证 $|T|=2$。
### 输出格式
对于每次询问,输出一行一个正整数表示答案。
### 数据范围
对于 $100\%$ 的数据,$1\le n,q\le 2\times 10^5,1\le l\le r\le n,S,T$ 中只含小写英文字母,$|T|=2$。
| 测试点编号 | $n$ | $q$ | 其他 |
| :---------: | :----------------: | :----------------: | :-----------------: |
| $1\sim 4$ | $\le 100$ | $\le 100$ | 无 |
| $5\sim 8$ | $\le 5000$ | $\le 5000$ | 无 |
| $9\sim 10$ | $\le 10^5$ | $\le 10^5$ | 所有的 $S_i$ 均相同 |
| $11\sim 12$ | $\le 10^5$ | $\le 3$ | 无 |
| $13\sim 16$ | $\le 10^5$ | $\le 10^5$ | 无 |
| $17\sim 20$ | $\le 2\times 10^5$ | $\le 2\times 10^5$ | 无 |
<div style="page-break-after: always"></div>
### 二、思路
#### 方法一
暴力循环 $[l,r]$,判断是否满足题意的数量,复杂度 $O(n^2q)$
#### 方法二
对于上面的方法,显然,其实我们可以只枚举有多少个满足 $S_j=T_2$,那么有多少个 $i$
满足 $S_i=T_1$ 是可以用前缀和预处理后 $O(1)$ 算出来的。复杂度 $O(nq)$
#### 方法三
这种方法是对方法二的一种小优化。
我们在枚举有多少个满足 $S_j=T_2$
时,我们其实是可以把 $S_i$ 按字母分成$26$类,在每一类中分别枚举的,这个过程可以用`vector`辅助实现。
#### 方法四
我们对方法三进行优化。
我们既然已经把 $S_i$ 分类了,而我们要求的是 $[l,r]$ 区间,我们就可以对分类后的 $S_i$ 作个前缀和,然后二分即可。复杂度 $O(qlogn)$
实现上有一些小细节要注意
### 三、$STL$二分版本
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 10;
int n, m; // n个长度的原串,m个询问
char S[N]; // 原串
vector<int> p[26]; // 记录每个字符出现的位置,比如'a'出现在位置1,3,5,...
vector<int> f[26][26]; // 三维数组,从谁,二维:到谁,三维:第几次出现,值:有多少个从谁
int s[N][26]; // 分类前缀和
// 使用STL版本的lower_bound,upper_bound
int main() {
freopen("ridge.in", "r", stdin);
freopen("ridge.out", "w", stdout);
cin >> n >> m >> (S + 1);
/* (1)因为题目数据范围很大,只能用O(NlogN)的时间复杂度(或更低)才能过掉,所以在输入数据时,必须千方百计的预处理提高性能
(2)从最后询问的问题来思考,设原串为S,开始字符为x、结束字符为y:
① 预处理出S中每个字符出现的位置p[],在询问时可以只枚举有用的位置,例:p[0]={2,4} 表示'a'出现在2,4两个位置。
② 预处理出S中每个字符出现的次数s[],可以用前缀和,例:s[10][0]=3 表示在S的前10个字符中,'a'字符出现了3次。
只有上面两个预处理出的数组还不行,因为串中可能多次出现a和b,但有些a在b后面,直接使用①②是不对的。
③ 预处理出:每当y出现时,记录前面已经出现过了多少个x,用数组vector<int> f[][][]来表示:
第一维代表是26个可能的来源字符x
第二维代表是26个可能的终止字符y
第三维代表是:第一次出现,第二次出现,....
值:x之前出现次数
④ 以f[][][]为基础数据,再次三层循环累加起来的值,表示:第k次出现时,前k个y与前面所有x的配对数量,是一个累加前缀和概念。
*/
for (int i = 1; i <= n; i++) {
int y = S[i] - 'a'; // 当前字符y
for (int x = 0; x < 26; x++) {
s[i][x] = s[i - 1][x];
f[x][y].push_back(s[i][x]); // x是肯定有的,每回26个,y是因为看到了当前的字符y。换句话说:就是y出来一次,就push_back了26个x的统计数据
}
p[y].push_back(i); // 字符y出现在i这个位置上
s[i][y]++; // 前i个字符中,字符y出现的次数增加了1个
}
// 计算区间前缀和
for (int i = 0; i < 26; i++) // 从字符i
for (int j = 0; j < 26; j++) // 到字符j
for (int k = 1; k < (int)f[i][j].size(); k++) // 有多条记录
f[i][j][k] += f[i][j][k - 1]; // 生成前缀和
while (m--) {
int l, r;
string t;
cin >> l >> r >> t;
int x = t[0] - 'a', y = t[1] - 'a';
int ql = lower_bound(p[y].begin(), p[y].end(), l) - p[y].begin(); // y的左边界
int qr = upper_bound(p[y].begin(), p[y].end(), r) - p[y].begin() - 1; // y的右边界
if (ql > qr) { // 如果没有找到y,输出0
cout << 0 << endl;
continue;
}
int cnt = qr - ql + 1; // y的个数
// 两层前缀和
// 以y的右边界结尾,计算y_右与前面所有x的配对数量=f[x][y][qr]
// 以y的左边界结尾,计算y_右与前面所有x的配对数量=f[x][y][ql - 1]
// 两者的差,还需要继续减去前l-1个中存在的x与[l,r]区间内的y的配对关系数量
// 需要注意的是:因为使用的是vector,下标从0开始,如果直接使用ql-1可能会有下标为负数的风险,需要判断一下
cout << f[x][y][qr] - (ql == 0 ? 0 : f[x][y][ql - 1]) - s[l - 1][x] * cnt << endl;
}
return 0;
}
```
### 四、手写二分版本
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 10;
int n, m;
char S[N];
vector<int> p[26];
vector<int> f[26][26];
int s[N][26];
int lower_bound(vector<int> q, int l, int r, int x) {
while (l < r) {
int mid = (l + r) >> 1;
if (q[mid] >= x)
r = mid;
else
l = mid + 1;
}
return l;
}
int upper_bound(vector<int> q, int l, int r, int x) {
while (l < r) {
int mid = (l + r) >> 1;
if (q[mid] > x)
r = mid;
else
l = mid + 1;
}
return l;
}
// 使用手写版本的lower_bound,upper_bound
int main() {
freopen("ridge.in", "r", stdin);
freopen("ridge.out", "w", stdout);
cin >> n >> m >> (S + 1);
for (int i = 1; i <= n; i++) {
int y = S[i] - 'a';
for (int x = 0; x < 26; x++) {
s[i][x] = s[i - 1][x];
f[x][y].push_back(s[i][x]);
}
p[y].push_back(i);
s[i][y]++;
}
for (int i = 0; i < 26; i++)
for (int j = 0; j < 26; j++)
for (int k = 1; k < (int)f[i][j].size(); k++)
f[i][j][k] += f[i][j][k - 1];
while (m--) {
int l, r;
string t;
cin >> l >> r >> t;
int x = t[0] - 'a', y = t[1] - 'a';
int ql = lower_bound(p[y], 0, (int)p[y].size(), l);
int qr = upper_bound(p[y], 0, (int)p[y].size(), r) - 1;
if (ql > qr) {
cout << 0 << endl;
continue;
}
int cnt = qr - ql + 1;
cout << f[x][y][qr] - (ql == 0 ? 0 : f[x][y][ql - 1]) - s[l - 1][x] * cnt << endl;
}
return 0;
}
```