说明:CSDN和公众号文章同步发布,需要第一时间收到最新内容,请关注公众号【比特正传】。
任何一种数据结构或者算法都是为解决某一个问题而诞生的,Trie树也是如此;
比如:给n个字符串,需要查询q个字符串,判断这q个字符串是否存在于n个字符串中,或者q个字符串是n个字符串中多少个字符串的前缀。如下图所示:
链接:【模板】字典树 - 洛谷
最简单的办法就是遍历n个字符串,然后依次判断,如果满足要求,数量+1,最终输出结果。但是看看题目数据范围,这样做肯定会超时,因此可以用Trie树来解决该问题。
Trie树
对于n个字符串的存储,可以共用相同的前缀,从而起到节省空间的效果。
比如要存储以下3个字符串,abcd, abd, bcd, 存储后如下图所示:
如上图所示,每个节点代表一个字母,root为根节点,可以看到每个节点旁边有个红色的数字,这是给每个节点的编号,根据插入顺序递增进行分配,该节点编号也是节点在数组中存储的下标,比如先插入abcd,那么a编号为1,b为2,c为3,d为4,至此就将abcd这个字符串插入到了Trie树,然后下一个插入abd,从root出发,由于a已经有了,因此复用,继续往下走,发现b也已经有了,则字符b也复用,继续走,发现d没有,那么新增一个结点,编号为5,以此类推,将以上三个字符串全部插入后,就生成了上面这棵Trie树。
那么问题来了?如果现在需要查询abc在不在上面的字符串中,怎么办?从root出发,搜索a,发现a在,继续搜索b,b也在,搜索c,c也在,那此时认为abc在不在呢?咱们很容易看出来,abc不在,abc是字符串abcd的前缀串,但是计算机不认识呀,所以我们还需要标记一下每个节点是不是结束节点,由于每个节点有编号,所以可以通过数组实现,定义一个数组cnt,假设3号节点是结束节点,那么cnt[3]++,这样的话,即使出现刚才的情况,由于cnt[3]为0,因此可以判断3号节点不是结束的结点,因此abc不在上面的字符串中,通过cnt数组,咱们也可以求出任意一个字符串出现的次数,假设上图中字符串abc出现了5次,那么cnt[3]一定等于5。
由于这个题目问的是前缀字符串的出现次数,那么只需要变动cnt数组就行,如果求字符串的出现次数,那么是到达每个字符串的最后一个字符的结点,才让cnt数组+1,求解前缀串的话就可以让cnt数组在字符串的每个字符位置处+1(因为是前缀串,所以可以认为在每个字符处均可以结束),这样就可以很简单的求出前缀字符串了。
代码如下:
#include "bits/stdc++.h"
using namespace std;
const int N = 3e6+7;
int t, n, q, a[N][62], cnt[N];
char s[N];
int idx = 0;
// 给一个大写或者小写或者数字字符,转为数组的下标
//
int getNum(char c) {
int u;
if(c >= 'a' && c <= 'z') u = c - 'a'; //小写字母在数组的0~25存储
else if(c >= 'A' && c <= 'Z') u = c - 'A' + 26; //大写字母在数组的26~51存储
else u = c - '0' + 52; //数字字符在数组的52~61存储
return u;
}
void clear() {
// 对cnt数组和a数组进行清空
for(int i=0; i<=idx; i++) {
cnt[i] = 0;
for(int j=0; j<62; ++j) {
a[i][j] = 0;
}
}
return;
}
void insert() {
int p = 0, u;
for(int i=0; i<strlen(s); ++i) { // 遍历字符串s,进行插入
u = getNum(s[i]); // 计算在数组下标的位置
if(a[p][u] == 0) a[p][u] = ++idx; // 如果之前没插入过,那么插入
p = a[p][u]; // 得到下一个字符的位置
cnt[p]++; // 由于是前缀串,所以每个字符的cnt都要+1,如果是找精确的字符串,那么cnt[p]++应该放到for循环外面
}
return;
}
int query() {
int p = 0, u;
for(int i=0; i<strlen(s); ++i) {
u = getNum(s[i]);
if(a[p][u] == 0) return 0;
p = a[p][u];
}
return cnt[p]; // 返回次数
}
int main() {
// 由于数据规模,本题统一用scanf 和 printf做输出
scanf("%d", &t);
while(t--) {
idx = 0; // 对新生成的Trie树的每个节点编号,记录当前编号
scanf("%d%d", &n, &q);
while(n--) {
scanf("%s", s);
insert(); // 由于字符数组s是全局变量,所以插入可以不传,insert函数共享数组s
}
while(q--) {
scanf("%s", s);
printf("%d\n", query());
}
clear(); // 下一组的时候需要清空cnt和a数组
}
return 0;
}
Do It Yourself!