|
|
|
|
## [计蒜客 $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;
|
|
|
|
|
}
|
|
|
|
|
```
|