You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

8.3 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

计蒜客 T3668 Eye of the Storm

一、题目描述

云浅来到了风暴的中心。这里漂浮着一个长为 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

二、思路

方法一

暴力循环 [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二分版本

#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;
}

四、手写二分版本

#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;
}