智乃的Notepad(Hard version)
智乃的Notepad(Hard version)
题目描述
本题有对应的Easy Version,区别仅在Easy为单组查询,Hard为多组,保证Easy的测试用例集为Hard的子集。
智乃在练习打字,她打开电脑上的记事本,准备输入一系列单词组成的集合,她想要知道如果想在记事本上显示过这个单词集合中所有的单词,则最少敲多少下键盘?
具体来讲,智乃只能使用键盘上的 个英文字母和退格符。在题干中,为了便于观察,我们使用 表示退格符,输入后会删除记事本上最后一个字符。
例如,当前的单词集合是 ,则在键盘上依次输入 就可以完成在记事本上显示过 三个单词的目标,只要求在输入的过程中存在过这几个单词就算完成目标,没有先后顺序。
现在有 个单词,编号从 到 ,智乃有 次查询,每次查询单词集合为 到 时,要想在记事本上显示过所有单词,至少要敲几下键盘?
注意:记事本上显示某个单词指的是在某个时刻,记事本上仅显示该单词,不包括以子串的形式显示。敲一次退格符视为敲一下键盘。
输入描述:
第一行输入两个正整数 代表单词的数目、查询的数目。
此后 行,第 行输入一个仅由小写英文字母组成的单词 。
最后输入 行,每行输入两个正整数 代表一次查询。
除此之外,保证单个测试文件中出现的小写字母数量 之和不超过 。
输出描述:
对于每一次查询,新起一行。输出一个整数,代表最少需要敲几次键盘。
示例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
解题思路
先考虑询问整个区间 的情况。一个贪心的想法是前缀相同的应该尽量放在一起处理,从而减少回退的次数。此时就会想到字典树 trie,它通过公共前缀来对字符串进行维护。把字符串 依次插入形成 trie,以任意的顺序去遍历整棵 trie,如果把向下递归看作是键入字符,向上回溯看作是回退字符,那么整个遍历的过程其实就是在模拟字符串的输入。并且这种方式一定是最优的,因为具有相同前缀的总是会相继处理。因此最小的按键次数就是 trie 中边数(记为 )的两倍再减去最长的字符串长度,即 。之所以要减去是因为题目没有要求最后要回退把所有字符删去,在 trie 中就是遍历到最后一个节点就可以直接结束,而不需要回溯到根节点,因此很自然想到最后再去处理长度最长的那个字符串。
如果询问的区间是 呢?我们当然可以像上面那样把 插入到 trie 中得到边数从而求得答案,但每个查询都这样做显然会超时。进一步思考,trie 中的哪些边对答案有贡献呢?在把字符串插入到 trie 中有些边会被重复经过,不妨给每条进行标记(每条边可能会有若干个不同的标记),在插入字符串 时所经过的每一条边都打上标记 。因此对于查询 ,实际上只有那具有 内标记的边才对答案有贡献。
如果先直接插入 得到 trie,再对每个询问统计具有 内标记的边的数量,貌似并不容易。因此想到能不能离线处理询问,考虑按右端点 从小到大来处理询问,并在这个过程中依次往 trie 中插入 。此时我们只需要考虑具有大于等于 的标记的边,等价于统计最大标记至少 的边的数量,也就是说我们只需维护每条边的最大标记即可。当插入字符串 时,只需把经过的边的最大标记更新为 即可(我们始终按编号从小到大插入字符串)。
剩下的问题就是如何统计最大标记大于等于 的边的数量,只需开一个数组统计每个最大标记对应的边的数量,然后用树状数组来维护这个数组即可。在往 trie 中插入字符串的时需要对树状数组进行更新和维护,详见代码。
另外我们还需要快速求得 内最大的字符串长度,这个只需要用 st 表维护即可。
AC 代码如下,时间复杂度为 :
#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
本文来自博客园,作者:onlyblues,转载请注明原文链接:https://www.cnblogs.com/onlyblues/p/18695956
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
2024-01-31 D. Blocking Elements
2023-01-31 C. Remove the Bracket