数据结构学习:字典树trie
字典树
又称单词查找树,Trie树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。
字典树的实现方法:
1.链、指针方式
2.自动机结构——数组实现
指针因为根据每个节点的儿子数量不同,不管是运用动态开点或者是静态设置指针数量都较为复杂,所以大多数情况下还是使用自动机结构更加简便
这里先直接给出一个字典树的模板:
给定一个单词列表,然后问你列表中是否存在某个单词(内有注释)
点击查看代码块
/*
trie树(前缀树)
trie[maxnNode][charSet]
trie[i][j] == 0 表示trie树中的第i号节点,没有连边
trie[i][j] == x 表示trie树中的第i号节点,与树中的第x号节点有一条连边,边权为字符集中的第j个字符
*/
#include <bits/stdc++.h>
using namespace std;
const int maxn=1e7+10;
const int charset = 30;
int trie[maxn][charset];
int tot;//trie中节点的个数
int color[maxn]={0};
int a[maxn];
//insert的时间复杂度 :O(len(s))
void insert(char *s){//在字典树中插入字符串s
int len = strlen(s);
int p = 0;//从根节点开始
for (int i=0;i<len;i++){
int c=s[i]-'a';
if(!trie[p][c]){//树中没有节点p向外连的边权为c的边
trie[p][c]=++tot;
}
a[trie[p][c]]++;
p=trie[p][c];
}
color[p]=1;//标记最后的节点为p
}
//search的时间复杂度 :O(len(s))
int search(char *s){//在字典树中查找s字符串
int len=strlen(s);
int p=0;
for (int i=0;i<len;i++){
int c = s[i]-'a';
if(!trie[p][c]) return 0;//字符不匹配,找不到这样的字符串
p = trie[p][c];
}
// return color[p]=1;//如果p的color为1,表明到达了叶子节点,说明这样的字符串存在
return a[p];
}
char ss[maxn];
int main(){
while(gets(ss)){
if(ss[0] == '\0') break;
insert(ss);
}
while(cin>>ss){
int ans = search(ss);
printf("%d\n",ans);
}
return 0;
}
可以看见,每次在字典树中插入一个字符串或者查询一个字符串的时间复杂度都是O(len(s)),len(s)表示字符串的长度。
例题:
1.单词数
题意:
一行一行读入字符串,统计该行有几个单词。单词之间只有空格分割。
做法:
建立字典树,在每次插入一个字符串的时候就可以判断当前是否之前有相同的字符串了,如果没有则答案+1,标记当前字符串出现过
点击查看代码块
/*
统计单词数,trie
*/
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5+10;
const int charset = 30;
int trie[maxn][charset];
int tot = 0;
int color[maxn];
int ans=0;
void insert(string s){
int len = s.length();
int p=0;
for (int i=0;i<len;i++){
int c=s[i]-'a';
if(!trie[p][c]) {
trie[p][c]=++tot;
}
p=trie[p][c];
}
if(color[p] == 0){
ans++;
color[p] = 1;
}
}
string s1,s2;
int main(){
while(getline(cin,s1)){
if(s1=="#") break;
ans = 0;
tot = 0;
memset(color,0,sizeof(color));
memset(trie,0,sizeof(trie));
stringstream ss(s1);
while(ss>>s2){
insert(s2);
}
printf("%d\n",ans);
}
return 0;
}
/*
you are my friend
you are my best best friend
#
*/
2.Shortest Prefixes
题意:
给你一些单词,让你求出他们最短的前缀,当然,这个前缀不能有歧义,例如给出单词 carton cart car
carton的前缀就不能是cart,因为cart的前缀是cart,同理cart的前缀也不能是car。
要找到每个单词独一无二且是最短的前缀,car的前缀不能是,”c“ “ca” ,因为他们在别的单词中也有出现,
如果找不到独一无二,那个这个单词本身就是他自己的前缀。
做法:
先将所有的字符串建立出一颗字典树,每插入一个字符串的时候记录当前节点出现的次数+1。
之后查询每个字符串的时候,从根节点往下找,如果有一个节点的次数为1,则表明该节点就是此字符唯一出现的地方,也就是独一无二的前缀
点击查看代码块
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <string>
#include <cstring>
using namespace std;
const int maxn=2e5+10;
const int SIZE = 30;
int trie[maxn][SIZE];
int tot=0;
int color[maxn];
void insert(string s){
int len=s.size();
int p=0;
for (int i=0;i<len;i++){
int c=s[i]-'a';
if(!trie[p][c]){
trie[p][c] = ++tot;
}
p=trie[p][c];
color[p]++;
}
}
string check(string s){
string ss="";
int len=s.size();
int p = 0;
for (int i=0;i<len;i++){
int c=s[i]-'a';
ss+=s[i];
p=trie[p][c];
if(color[p] == 1){
return ss;
}
}
}
string s[1010];
int main(){
int cnt=0;
while(cin>>s[cnt]){
insert(s[cnt]);
cnt++;
}
for (int i=0;i<cnt;i++){
string ans = check(s[i]);
cout<<s[i]<<" "<<ans<<endl;
}
return 0;
}
3.Phone List
题意:
给定若干组电话号码列表,对于每组列表,如果有一个号码是另外一个号码的前缀,输出“NO”,如果不存在这样的电话号码,输出“YES”
做法:
根据所有的电话号码建立字典树,在字典树上标记每个电话号码的最后一个位置。
然后查询每个号码,每次查号码的前n-1个位置,如果发现在查询过程中某个节点已经被标记,
表明该节点是某个电话号码的结尾,也就是说明有某个电话号码是当前查询的电话号码的前缀,输出NO,如果所有的电话号码都找不到,输出YES
点击查看代码块
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <string>
#include <cstring>
using namespace std;
const int maxn=2e5+10;
const int SIZE = 15;
int n;
int trie[maxn][SIZE];
int tot=0;
int color[maxn];
void insert(string s){
int len=s.size();
int p=0;
for (int i=0;i<len;i++){
int c=s[i]-'0';
if(!trie[p][c]){
trie[p][c] = ++tot;
}
p=trie[p][c];
}
color[p] = 1;
}
bool check(string s){
int len=s.size()-1;
int p=0;
for (int i=0;i<len;i++){
int c=s[i]-'0';
p=trie[p][c];
if(color[p] == 1) return true;
}
return false;
}
string s[maxn];
int main(){
int T;
cin>>T;
while(T--){
tot=0;
scanf("%d",&n);
for (int i=0;i<maxn;i++){
color[i] = 0;
for (int j=0;j<12;j++){
trie[i][j]=0;
}
}
bool flag = 0;
for (int i=1;i<=n;i++){
cin>>s[i];
insert(s[i]);
}
for (int i=1;i<=n;i++){
bool ok = check(s[i]);
if(ok) {
flag = 1;break;
}
}
if(flag) puts("NO");
else puts("YES");
}
return 0;
}
4.01字典树:求区间异或和最大值和最小值 Consecutive Sum
给定n个数,求区间的最大和最小异或和
做法:
将从1-n,对于每个a[i]根统计前缀异或和,然后先查询,再对每个数根据二进制从高到低位建立01字典树
具体操作看代码:
点击查看代码块
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <string>
using namespace std;
const int maxn=3e6+10;
const int inf=0x7fffffff;
int trie[maxn][2];
int tot = 0;
int T;
int n;
int a[maxn];
int sum[maxn];//前缀异或和
int ans1,ans2;
int Min,Max;
int color[maxn];
int read(){
int s = 0,f=1;
char c=getchar();
while(c<'0' || c>'9'){
if(c=='-') f=-1;
c=getchar();
}
while(c>='0' && c<='9'){
s=s*10+c-'0';
c=getchar();
}
return f*s;
}
void insert(int num){
int p=0;
int c;
for (int i=30;i>=0;i--){
if((num>>i)&1) c=1;
else c=0;
if(!trie[p][c]) trie[p][c]=++tot;
p=trie[p][c];
}
}
void search(int num){
int p1=0,p2=0;
int c;
ans1=ans2=0;
for (int i=30;i>=0;i--){
if((num>>i)&1) c=1;
else c=0;
if(trie[p1][c^1]){//找最大值,就要找与之相反的比如0找1,1找0,才能保证最大
ans1^=(1<<i);
p1=trie[p1][c^1];
}
else {
p1=trie[p1][c];
}
if(trie[p2][c]){//找最小值,就要找与之相同的
p2=trie[p2][c];
}
else {
ans2^=(1<<i);
p2=trie[p2][c^1];
}
}
return ;
}
int main(){
cin>>T;
int Case = 0;
while(T--){
tot=0;
Min=inf-1;
Max=0;
scanf("%d",&n);
insert(0);
for (int i=1;i<=n;i++){
a[i]=read();
sum[i] = sum[i-1] ^ a[i];
search(sum[i]);
Max=max(Max,ans1);
Min=min(Min,ans2);
// cout<<"Max = "<<Max<<" Min = "<<Min<<endl;
insert(sum[i]);
}
printf("Case %d: %d %d\n",++Case,Max,Min);
for (int i=0;i<=tot;i++){
trie[i][1]=trie[i][0]=0;
}
}
return 0;
}
/*
2
5
6 8 2 4 2
5
3 8 2 6 5
*/
5.luogu P4551 最长异或路径
题意:
给定一棵n个点的带权树,结点下标从1开始到N。寻找树中找两个结点,求最长的异或路径。
异或路径指的是指两个结点之间唯一路径上的所有边权的异或。
做法:
先建树,然后dfs记录每个节点到根节点的前缀异或和,
之后根据每个节点的异或和建立字典树,之后查询即可
点击查看代码块
#include <bits/stdc++.h>
using namespace std;
const int maxn=2e5+10;
int n;
struct edge{
int u,v,next;
int w;
}e[maxn];
int head[maxn],cnt=0;
int a[maxn];//每个点到根节点的异或和
int trie[maxn*32][2],tot=0;
int Max = 0;
void add(int u,int v,int w){
e[cnt].u=u;
e[cnt].v=v;
e[cnt].w=w;
e[cnt].next=head[u];
head[u]=cnt++;
}
void dfs(int u,int f){//处理每个点到根节点的路径的前缀异或和
for (int i=head[u];~i;i=e[i].next){
int v=e[i].v;
if(v!=f){
a[v]=a[u]^e[i].w;
dfs(v,u);
}
}
}
void build(int x){
int p=0;
int c;
for (int i=31;i>=0;i--){
if((x>>i)&1) c=1;
else c=0;
if(!trie[p][c]){
trie[p][c]=++tot;
}
p=trie[p][c];
}
}
int query(int x){
int p=0;
int c;
int ans = 0;
for (int i=31;i>=0;i--){
if((x>>i)&1) c=1;
else c=0;
if(trie[p][c^1]){
ans+=(1<<i);
p=trie[p][c^1];
}
else p=trie[p][c];
}
return ans;
}
int main(){
memset(head,-1,sizeof(head));
cin>>n;
for (int i=1;i<n;i++){
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
add(u,v,w);add(v,u,w);
}
dfs(1,0);
for (int i=1;i<=n;i++){
build(a[i]);
}
Max = 0;
for (int i=1;i<=n;i++){
Max=max(Max,query(a[i]));
}
printf("%d\n",Max);
return 0;
}