Processing math: 55%

智乃的Notepad(Hard version)

智乃的Notepad(Hard version)

题目描述

本题有对应的Easy Version,区别仅在Easy为单组查询,Hard为多组,保证Easy的测试用例集为Hard的子集。

智乃在练习打字,她打开电脑上的记事本,准备输入一系列单词组成的集合,她想要知道如果想在记事本上显示过这个单词集合中所有的单词,则最少敲多少下键盘?

具体来讲,智乃只能使用键盘上的 26 个英文字母和退格符。在题干中,为了便于观察,我们使用 b 表示退格符,输入后会删除记事本上最后一个字符。

例如,当前的单词集合是 {nowcoder,nowdays,now},则在键盘上依次输入 nowdaysbbbbcoder 就可以完成在记事本上显示过 nowcoder,nowdays,now 三个单词的目标,只要求在输入的过程中存在过这几个单词就算完成目标,没有先后顺序。

现在有 n 个单词,编号从 1n,智乃有 m 次查询,每次查询单词集合为 lr 时,要想在记事本上显示过所有单词,至少要敲几下键盘?

注意:记事本上显示某个单词指的是在某个时刻,记事本上仅显示该单词,不包括以子串的形式显示。敲一次退格符视为敲一下键盘。

输入描述:

第一行输入两个正整数 n,m(1n,m105) 代表单词的数目、查询的数目。

此后 n 行,第 i 行输入一个仅由小写英文字母组成的单词 si

最后输入 m 行,每行输入两个正整数 li,ri(1lirin) 代表一次查询。

除此之外,保证单个测试文件中出现的小写字母数量 |si| 之和不超过 106

输出描述:

对于每一次查询,新起一行。输出一个整数,代表最少需要敲几次键盘。

示例1

输入

3 3
nowcoder
nowdays
now
1 3
1 2
3 3

输出

16
16
3

示例2

输入

4 1
nowcoder
nowdays
days
coder
1 4

输出

34

 

解题思路

  先考虑询问整个区间 [1,n] 的情况。一个贪心的想法是前缀相同的应该尽量放在一起处理,从而减少回退的次数。此时就会想到字典树 trie,它通过公共前缀来对字符串进行维护。把字符串 s1,,sn 依次插入形成 trie,以任意的顺序去遍历整棵 trie,如果把向下递归看作是键入字符,向上回溯看作是回退字符,那么整个遍历的过程其实就是在模拟字符串的输入。并且这种方式一定是最优的,因为具有相同前缀的总是会相继处理。因此最小的按键次数就是 trie 中边数(记为 e)的两倍再减去最长的字符串长度,即 2emax。之所以要减去是因为题目没有要求最后要回退把所有字符删去,在 trie 中就是遍历到最后一个节点就可以直接结束,而不需要回溯到根节点,因此很自然想到最后再去处理长度最长的那个字符串。

  如果询问的区间是 [l,r] 呢?我们当然可以像上面那样把 s_l, \ldots s_r 插入到 trie 中得到边数从而求得答案,但每个查询都这样做显然会超时。进一步思考,trie 中的哪些边对答案有贡献呢?在把字符串插入到 trie 中有些边会被重复经过,不妨给每条进行标记(每条边可能会有若干个不同的标记),在插入字符串 s_i 时所经过的每一条边都打上标记 i。因此对于查询 [l,r],实际上只有那具有 [l,r] 内标记的边才对答案有贡献。

  如果先直接插入 s_1, \ldots, s_n 得到 trie,再对每个询问统计具有 [l,r] 内标记的边的数量,貌似并不容易。因此想到能不能离线处理询问,考虑按右端点 r 从小到大来处理询问,并在这个过程中依次往 trie 中插入 s_1, \ldots, s_r。此时我们只需要考虑具有大于等于 l 的标记的边,等价于统计最大标记至少 l 的边的数量,也就是说我们只需维护每条边的最大标记即可。当插入字符串 s_i 时,只需把经过的边的最大标记更新为 i 即可(我们始终按编号从小到大插入字符串)。

  剩下的问题就是如何统计最大标记大于等于 l 的边的数量,只需开一个数组统计每个最大标记对应的边的数量,然后用树状数组来维护这个数组即可。在往 trie 中插入字符串的时需要对树状数组进行更新和维护,详见代码。

  另外我们还需要快速求得 [l,r] 内最大的字符串长度,这个只需要用 st 表维护即可。

  AC 代码如下,时间复杂度为 O\left(n \log{n} + m (\log{m} + \log{n}) + \sum{|s_i|} \log{n}\right)

#include <bits/stdc++.h>
using namespace std;

typedef long long LL;

const int N = 1e5 + 5, M = 1e6 + 5;

int n, m;
string s[N];
int l[N], r[N], p[N];
int tr1[M][26], c[M], idx;
int tr2[N];
int f[17][N];
int ans[N];

int lowbit(int x) {
    return x & -x;
}

void add(int x, int c) {
    for (int i = x; i <= n; i += lowbit(i)) {
        tr2[i] += c;
    }
}

int query(int x) {
    int ret = 0;
    for (int i = x; i; i -= lowbit(i)) {
        ret += tr2[i];
    }
    return ret;
}

void add(string &s, int x) {
    int p = 0;
    for (int i = 0; i < s.size(); i++) {
        int t = s[i] - 'a';
        if (!tr1[p][t]) tr1[p][t] = ++idx;
        p = tr1[p][t];
        if (c[p]) add(c[p], -1);
        c[p] = x;
        add(c[p], 1);
    }
}

int query(int l, int r) {
    int t = __lg(r - l + 1);
    return max(f[t][l], f[t][r - (1 << t) + 1]);
}

int main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        cin >> s[i];
    }
    for (int i = 0; i < m; i++) {
        cin >> l[i] >> r[i];
        p[i] = i;
    }
    sort(p, p + m, [&](int i, int j) {
        return r[i] < r[j];
    });
    for (int i = 0; 1 << i <= n; i++) {
        for (int j = 1; j + (1 << i) - 1 <= n; j++) {
            if (!i) f[i][j] = s[j].size();
            else f[i][j] = max(f[i - 1][j], f[i - 1][j + (1 << i - 1)]);
        }
    }
    for (int i = 1, j = 0; i <= n; i++) {
        add(s[i], i);
        while (j < m && r[p[j]] == i) {
            ans[p[j]] = 2 * (query(r[p[j]]) - query(l[p[j]] - 1)) - query(l[p[j]], r[p[j]]);
            j++;
        }
    }
    for (int i = 0; i < m; i++) {
        cout << ans[i] << '\n';
    }
    
    return 0;
}

 

参考资料

  【题解】2025牛客寒假算法基础集训营3:https://ac.nowcoder.com/discuss/1453293

posted @   onlyblues  阅读(11)  评论(0编辑  收藏  举报
Web Analytics
点击右上角即可分享
微信分享提示