智乃的Notepad(Hard version)
智乃的Notepad(Hard version)
题目描述
本题有对应的Easy Version,区别仅在Easy为单组查询,Hard为多组,保证Easy的测试用例集为Hard的子集。
智乃在练习打字,她打开电脑上的记事本,准备输入一系列单词组成的集合,她想要知道如果想在记事本上显示过这个单词集合中所有的单词,则最少敲多少下键盘?
具体来讲,智乃只能使用键盘上的 26 个英文字母和退格符。在题干中,为了便于观察,我们使用 ∖b 表示退格符,输入后会删除记事本上最后一个字符。
例如,当前的单词集合是 {nowcoder,nowdays,now},则在键盘上依次输入 nowdays∖b∖b∖b∖bcoder 就可以完成在记事本上显示过 nowcoder,nowdays,now 三个单词的目标,只要求在输入的过程中存在过这几个单词就算完成目标,没有先后顺序。
现在有 n 个单词,编号从 1 到 n,智乃有 m 次查询,每次查询单词集合为 l 到 r 时,要想在记事本上显示过所有单词,至少要敲几下键盘?
注意:记事本上显示某个单词指的是在某个时刻,记事本上仅显示该单词,不包括以子串的形式显示。敲一次退格符视为敲一下键盘。
输入描述:
第一行输入两个正整数 n,m(1≤n,m≤105) 代表单词的数目、查询的数目。
此后 n 行,第 i 行输入一个仅由小写英文字母组成的单词 si。
最后输入 m 行,每行输入两个正整数 li,ri(1≤li≤ri≤n) 代表一次查询。
除此之外,保证单个测试文件中出现的小写字母数量 ∑|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)的两倍再减去最长的字符串长度,即 2e−max。之所以要减去是因为题目没有要求最后要回退把所有字符删去,在 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
本文来自博客园,作者:onlyblues,转载请注明原文链接:https://www.cnblogs.com/onlyblues/p/18695956
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· Obsidian + DeepSeek:免费 AI 助力你的知识管理,让你的笔记飞起来!
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
2024-01-31 D. Blocking Elements
2023-01-31 C. Remove the Bracket