计蒜客 T3668 Eye of the Storm

计蒜客 T3668 Eye of the Storm

一、题目描述

云浅来到了风暴的中心。这里漂浮着一个长为 n 的,由小写字母组成的字符串 S字符串的下标从 1 开始。

想要逃出风暴,就需要回答一些询问。

每次询问会给出一对正整数 l,r 和一个字符串 T,云浅需要回答 Slr 这段子串内有多少个子序列是 T这里保证 T 的长度为 2

形式化地,你需要求出有多少对 (i,j) 满足 li<jr,使得 Si=T1,Sj=T2

现在云浅预测出了风暴在接下来 q 个时刻内的询问,你需要帮她求出每个询问的答案。

输入格式

第一行两个正整数 n,q

第二行一个长为 n 的字符串 S

接下来 q 行,每行会给出两个正整数 l,r 和一个字符串 T,表示云浅需要回答的询问。保证 |T|=2

输出格式

对于每次询问,输出一行一个正整数表示答案。

数据范围

对于 100% 的数据,1n,q2×105,1lrn,S,T 中只含小写英文字母,|T|=2

测试点编号 n q 其他
14 100 100
58 5000 5000
910 105 105 所有的 Si 均相同
1112 105 3
1316 105 105
1720 2×105 2×105

二、思路

方法一

暴力循环 [l,r],判断是否满足题意的数量,复杂度 O(n2q)

方法二

对于上面的方法,显然,其实我们可以只枚举有多少个满足 Sj=T2,那么有多少个 i
满足 Si=T1 是可以用前缀和预处理后 O(1) 算出来的。复杂度 O(nq)

方法三

这种方法是对方法二的一种小优化。

我们在枚举有多少个满足 Sj=T2
时,我们其实是可以把 Si 按字母分成26类,在每一类中分别枚举的,这个过程可以用vector辅助实现。

方法四

我们对方法三进行优化。

我们既然已经把 Si 分类了,而我们要求的是 [l,r] 区间,我们就可以对分类后的 Si 作个前缀和,然后二分即可。复杂度 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;
}
posted @   糖豆爸爸  阅读(40)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
历史上的今天:
2022-03-15 AcWing 1135. 新年好
2022-03-15 AcWing 903. 昂贵的聘礼
Live2D
点击右上角即可分享
微信分享提示