Trie树(字典树)就是这么简单

 说明: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!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值