Tire树

Tire树,也叫做字典数,是一种存储字符串集合的方式。比起用二维数组之类的存储方法,Trie树的储存方法更加节省空间,而且在存储和查找的过程中,也十分的高效。
看似很厉害的东西,其实理解和学习起来并不算难,下面就一起康康,Trie树的存储方式以及代码的实现吧QwQ

1. Trie树的存储方式

顾名思义,Trie树既然是一种树,那么它的存储方式就一定是和树有关的。
Tire树的存储,其实就是建立一棵完全由字符组成的一棵树,树的深度也对应表示着字符串的长度。在储存一个新的字符串时,我们从根节点的子节点开始找起,看子节点中有没有已经存储了与字符串中的第一个字符相同的字符,如果有,就直接跳到那个字符的位置上,如果没有,就建立一个新的节点。

直接像这样说是很难理解的,所以我们来模拟一下这个过程。在Trie树中,存储“abcde”,“abcdf”,“abc”
“bcdf”四个字符串。

首先,我们把第一个字符串,“abcde”放入树中。因为这时候树是空的,所以所有节点都是新建的

Tire树_第1张图片
就像这样,再在这个字符串的末尾做上标记,然后开始存入第二个字符串“abcdf”
Tire树_第2张图片
就像这样,因为一直到‘d’时,两个字符串才出现差别,所以再加一个指向‘f’的分支就可以了。
然后接着插入“abc”
Tire树_第3张图片
因为“abc”这三个字符在之前的树中都按顺序出现过了,也就是说这个字符串是之前插入过的字符串的子串,我们就直接在这个字符 串的结尾位置做上标记就好了。
最后加入“bcdf”
Tire树_第4张图片
因为以‘b’开头的字符串我们还没遇见过,所以我们直接从根节点创建一个新的分支。
就这样,我们可以把很多字符串变成一个树存储起来。但是这样的储存方式必须保证树的分支不能太多,所以一般用Tire树的前提是要保证我们字符串中的不同字符个数是较少的。一般,我们可以用Trie树来存储英文单词,数字这些不过字符个数较少的字符串。

2. Tire树的代码实现

理解了上面的原理,代码实现也就是理解对应的步骤然后记忆模板就好了,所以我们直接来康Tire树的模板。

const int N = 100010;
int son[N][26], idx;  // 这里我们举例存储字母‘a’ - ‘z’,son指向子节点,idx代表下标
int vis[N]  // 用来标记字符串的结尾

void insert(char str[])  // 字符串的插入
{
	int p = 0;
	for(int i = 0; str[i]; i ++)
	{
		int u = str[i] - 'a';
		if(!son[p][u]) son[p][u] = ++ idx;   // 如果没找到,插入新节点
		p = son[p][u];  // 跳到下个节点的位置
	}
	vis[p] = 1;  // 标记字符串的结尾
}

// 查询字符串是否已经存储
// 查询和插入的思想很相似,基本再抄一遍就欧克了
bool query(char str[]) 
{
	int p = 0;
	for(int i = 0; str[i]; i ++)
	{
		int u = str[i] - 'a';
		if(!son[p][u]) return false;
		p = son[p][u];
	}
	if(vis[p]) return true;
	else return false;
}

好了,这就Trie树的核心代码,还是蛮短的。

3.做俩倒题

<1> Trie字符串统计

维护一个字符串集合,支持两种操作:

“I x”向集合中插入一个字符串x;
“Q x”询问一个字符串在集合中出现了多少次。

共有N个操作,输入的字符串总长度不超过 105105,字符串仅包含小写英文字母。

输入格式
第一行包含整数N,表示操作数。
接下来N行,每行包含一个操作指令,指令为”I x”或”Q x”中的一种。

输出格式
对于每个询问指令”Q x”,都要输出一个整数作为结果,表示x在集合中出现的次数。
每个结果占一行。

数据范围
1≤N≤2∗1041≤N≤2∗104

输入样例:
5
I abc
Q abc
Q ab
I ab
Q ab

输出样例:
1
0
1
这是一道简单的模板题了,直接给AC的代码。

#include
using namespace std;

const int N = 100010;
int son[N][26], cnt[N], idx; // cnt用来存出现的次数
char st[N], op[2];
int n;

void insert(char str[])
{
	int p = 0;
	for(int i = 0; str[i]; i ++)
	{
		int u = str[i] - 'a';
		if(!son[p][u]) son[p][u] = ++ idx;
		p = son[p][u];
	}
	cnt[p] ++;
}

int query(char str[])
{
	int p = 0;
	for(int i = 0; str[i]; i ++)
	{
		int u = str[i] - 'a';
		if(!son[p][u]) return 0;
		p = son[p][u];
	}
	return cnt[p];
}

int main()
{
	cin.tie(0); cout.tie(0);
	cin >> n;
	while(n --)
	{
		scanf("%s%s", op, st);
		if(op[0] == 'I') insert(st);
		else cout << query(st) << endl;
	}
	return 0;
}

<2>最大异或对

在给定的N个整数A1,A2……ANA1,A2……AN中选出两个进行xor(异或)运算,得到的结果最大是多少?

输入格式
第一行输入一个整数N。
第二行输入N个整数A1A1~ANAN。

输出格式
输出一个整数表示答案。

数据范围
1≤N≤1051≤N≤105,
0≤Ai<2310≤Ai<231

输入样例:
3
1 2 3

输出样例:
3

这一道题就有亿点点的技巧性。暴力的双重循环异或当然是没问题的,很好考虑,只是会TLE。我们现在把数放在二进制的环境下,我们要求一个数与之匹配的最大异或树,其实就要求二进制的每一位都尽量不一样。所以先把所有数的二进制形式存进树中,然后在找到每一个数的最大异或数,这样我们就可以用一重循环来找出我们的答案了。

#include
using namespace std;

const int N = 100010, M = 3000010;
int son[M][2], idx;
int a[N], n;

void insert(int x)
{
	int p = 0;
	for(int i = 30; ~i; i --)
	{
		int &s = son[p][x >> i & 1];
		if(!s) s = ++ idx;
		p = s;
	}
}

int query(int x)
{
	int p = 0, res = 0;
	for(int i = 30; ~i; i --)
	{
		int s = x >> i & 1;
		if(son[p][!s])
		{ 	
			res += 1 << i;
			p = son[p][!s];
		}
		else p = son[p][s];
	}
	return res;
}

int main()
{
	cin.tie(0); cout.tie(0);
	cin >> n;
	for(int i = 0; i < n; i ++)
	{
		cin >> a[i];
		insert(a[i]);
	}
	int ans = 0;
	for(int i = 0; i < n ; i ++) ans = max(ans, query(a[i]));
	cout << ans << endl;
	return 0;
}

嗯,就是这样,基本数据结构的知识点到这几天也基本整理完了(终于完了,人都快傻了,呜呜QwQ)
其实模板都不难,难的是变形和应用。。所以,还是好好刷题吧QwQ

你可能感兴趣的:(Tire树)