Trie树(字典树)
前言
笔者写这篇博客主要是方便以后的复习
一、概念
高效的存储和查找字符串集合的数据结构
二、插入与查找
2.1 插入
把trie树中结尾的字母打上一个标记,如果将要存储的字符串是已经存储了的字符串的子集,就在该字符串的末尾字母处打上一个标记,例如:上图中的abc
2.2 查找
下面给出查找过程:
比如要在下图中查找aced和abcf:
如果要查找abcd,虽然能到达abcd,因为我们存储了abcdef,但是因为d没有标记,所以找不到,因为我们没有存储abcd
三、实现代码
3.1 例题一
#include<bits/stdc++.h>
using namespace std;
const int N = 100010;
int son[N][26];//因为是小写英文字母,那么每个节点最多向外连26条边
int cnt[N],idx;//下标是0的点,既是根节点,又是空节点
char s[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()
{
int n;
cin>>n;
while (n -- )
{
char c;
cin>>c>>s;
if(c=='I')
{
insert(s);
}
else
{
cout<<query(s)<<endl;
}
}
}
3.2 例题二
先考虑暴力:
显然可以枚举两个元素a[i]和a[j],
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e5+10;
int a[N],n;
int main()
{
cin>>n;
int res = 0;
for(int i = 0;i<n;i++)
cin>>a[i];
for(int i = 0;i<n;i++)
{
for(int j = 0;j<i;j++)
res = max(res,a[i]^a[j]);
}
cout<<res;
}
显然O(n^2)会超时,观察知道至少要优化到O(nlogn)的时间复杂度,考虑能否优化第二重循环,即每次log(n)得到a[i]的最优组合的那个数,尝试建01trie树。
对于每个数字a[i],都是去寻找它的最优组合,那么如何确定最优组合呢?
因为是求异或,所以在query的时候,只要每次尽量走与a[i]的当前位相反的方向即可。
而建立01trie树和建立普通trie树是类似的。
ac code:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e5+10,M = 31*N;
int a[M],n;
int son[M][2];//最大有32位
int idx;
void insert(int x)
{
int p = 0;
for(int i = 30;i>=0;i--)
{
int u = x>>i&1;
if(!son[p][u]) son[p][u] = ++idx;
p = son[p][u];
}
}
int query(int x)
{
int p = 0,res = 0;//这里的res就是存的a[i]要找的那个人
for(int i = 30;i>=0;i--)
{
int u = x>>i&1;
if(son[p][!u])
{
p = son[p][!u];
res = res*2+!u;//能往相反方向走那就往相反方向走,因为异或是相同为0,不同为1
}
else
{
p = son[p][u];
res = res*2+u;
}
}
return res;
}
int main()
{
cin>>n;
for(int i = 0;i<n;i++)
{
cin>>a[i];
}
int res = 0;
for(int i = 0;i<n;i++)
{
insert(a[i]);
}
for(int i = 0;i<n;i++)
{
int t = query(a[i]);//t就是a[i]的命中注定那个人
res = max(res,a[i]^t);
}
cout<<res;//这里是全局最大值
return 0;
}
3.3 例题三
UVA11362 Phone List
只涉及到insert操作,用两个数组分别标记每次插入后的点和插入过程中的每个点,如果在插入过程中,遇到了前面已经插入的字符串的结尾,那么显然有解,如果插入完后,发现当前结尾这个点已经被访问过,说明它是前面插入的字符串的子串,显然有解。
#include<bits/stdc++.h>
using namespace std;
const int N = 100010;
int son[N][26];//因为是小写英文字母,那么每个节点最多向外连26条边
int idx;//下标是0的点,既是根节点,又是空节点
int n;
bool flag;
bool vis[N];
bool st[N];
void insert(string str)
{
int p = 0;
for(int i = 0;str[i];i++)
{
int u = str[i]-'0';
if(!son[p][u])
{
son[p][u] = ++idx;
st[p] = true;
}
if(vis[p])
{
flag = true;
return ;
}
p = son[p][u];
}
if(st[p]){
flag = true;
}
vis[p] = true;
}
int main()
{
int t;
cin>>t;
while(t--)
{
memset(son,0,sizeof son);
memset(vis,0,sizeof vis);
memset(st, 0, sizeof st);
idx = 0;
cin>>n;
flag = false;
while(n--)
{
string s;
cin>>s;
insert(s);
}
if(flag)
{
cout<<"NO\n";
}
else
{
cout<<"YES\n";
}
}
return 0;
}
未完待续~