#include using namespace std; const int N = 2e5 + 10; int n, m; // n个长度的原串,m个询问 char S[N]; // 原串 vector p[26]; // 记录每个字符出现的位置,比如'a'出现在位置1,3,5,... vector 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 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; }