字典树
算法思想
字典树很大程度上利用了单词之间的存在公共前缀的性质,是一种树形结构,查询效率高于哈希树,但是需要大量的内存空间,因为每一层/节点只存了一个信息,每个节点包含多个字符指针,将从根节点到某一路径上经过的字符连接起来就是该节点对应的字符串,具体分析略
插入代码如下
void Insert(char* s)
{
int len=strlen(s),p=1;
for(int i=0;i<len;i++)
{
ch=s[i]-'a';
if(!trie[p][ch])
trie[p][ch]=++tot;
p=trie[p][ch];
}
End[p]=1;
}
查询代码如下
bool Query(char*s)
{
int len=strlen(s),p=1;
for(int i=0;i<len;i++)
{
ch=s[i]-'a';
if(!trie[p][ch])
return 0;
p=trie[p][ch];
}
if(!End[p])
return 0;
return 1;
}
训练
POJ2503
题目大意:给出一些单词对,每个单词对第一个为翻译,第二个为原单词,输入一些单词,输出每个单词的翻译
思路:直接使用字典树存储原单词,同时存储翻译,存储完原单词后将原单词最后一个字母所在层与翻译所在编号对应建立索引,之后根据输入查询即可
代码
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <cstdio>
using namespace std;
int trie[121212][27],value[121212],tot=1;
char str[121],input[121212][121],s[121];
bool End[121212];
void Insert(int k) {
int len=strlen(str),p=1;
for(int i=0; i<len; i++) {
int ch=str[i]-'a';
if(!trie[p][ch])
trie[p][ch]=++tot;//tot为一共用了几个数组
p=trie[p][ch];//跳转到下一个
}
value[p]=k;//记录这一个数组对应哪一个单词
End[p]=1;//代表这一层数组有一个单词
}
int query() {
int len=strlen(str),p=1;
for(int i=0; i<len; i++) {
int ch=str[i]-'a';
p=trie[p][ch];//跳转到下一个
if(!p)//如果没找到,代表没有存储过
return 0;
}
return End[p]?value[p]:0;
}
int main() {
int i=1;
while(1) {
gets(s);//处理换行的方法
if(s[0]=='\0')
break;
sscanf(s,"%s %s",input[i],str);//同上注释
Insert(i++);
}
while(gets(str)) {
if(str[0]=='\0')
break;
int t=query();
if(t)
printf("%s\n",input[t]);
else
printf("eh\n");
}
return 0;
}
POJ3630
题目大意:给出多个数字序列,判断是否存在某个序列为另一个序列的前缀
思路:将每个字符串插入字典树中,如果插入的字符串经过处理后所在层数不为空,表示插入字符串为其他某字符串前缀,如果插入的字符串在处理的过程中某一层判断End为1(即在这一层有单词),说明插入字符串在字典树中有前缀
代码
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <cstdio>
using namespace std;
int trie[121212][12],T,N,tot=1;
char str[20];
bool End[121212];
bool Insert() {
int len=strlen(str),p=1;
for(int i=0; i<len; i++) {
int ch=str[i]-'0';//获得这一位数字
if(!trie[p][ch])
trie[p][ch]=++tot;//开新的一层
else if(i==len-1)//若字符串处理完仍不为空,代表该串为某一串前缀
return 1;
p=trie[p][ch];
if(End[p])//如果在处理的中间存在单词,代表有单词为该串前缀
return 1;
}
End[p]=1;
return 0;
}
int main() {
scanf("%d",&T);
while(T--) {
scanf("%d",&N);
bool flag=0;
tot=1;//记得更新
while(N--) {
scanf("%s",str);
if(flag)
continue;
flag=Insert();
}
if(flag)
printf("NO\n");
else
printf("YES\n");
memset(trie,0,sizeof(trie));
memset(End,0,sizeof(End));
}
return 0;
}
HDU1251
题目大意:给出只由小写字母构成的多个字符串,统计给定输入的以某个字符串为前缀的字符串数量
思路:先建立字典树,但是把End=1改成End++,用来统计到这一层的单词数,对于输入的查找字符串,在字典树中遍历,返回最后一个字母所在层数的End值即可,找不到就返回0
代码
#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <cstring>
using namespace std;
int trie[1212121][27],End[1212121],tot=1;
char s[12];
void Insert() {
int len=strlen(s),p=1;
for(int i=0; i<len; i++) {
int ch=s[i]-'a';
if(!trie[p][ch])
trie[p][ch]=++tot;
p=trie[p][ch];//需要先跳转,后统计,因为第一层是每个都要经过的,没有意义
End[p]++;
}
}
int Query() {
int len=strlen(s),p=1;
for(int i=0; i<len; i++) {
int ch=s[i]-'a';
if(!trie[p][ch])
return 0;
p=trie[p][ch];
}
return End[p];
}
int main() {
while(gets(s)) {
if(s[0]=='\0')
break;
Insert();
}
while(gets(s)) {
if(s[0]=='\0')
break;
printf("%d\n",Query());
}
return 0;
}
POJ2513
题目大意:给出一些单词对,代表木棒两端的颜色,判断能不能相同颜色连一端连成一条线
思路:首先给出欧拉回路相关的知识
经过图G的每一边恰好一次的路径为欧拉通路,若一个回路为欧拉通路,则称之为欧拉回路,有欧拉回路的图叫欧拉图,只有欧拉通路的图为半欧拉图
一个无向图存在欧拉回路,当且仅当连通且无奇度节点
一个有向图存在欧拉回路,当且仅当连通且所有节点的入度等于出度
一个无向图存在欧拉通路,当且仅当连通且无奇度节点或恰好两个奇度节点
一个有向图存在欧拉通路,当且仅当连通且所有节点入度都等于出度或恰好有两个节点一个出度-入度=1,另一个入度-出度=1
本题是对于给定图判断是否存在欧拉通路,将单词对通过字典树变成数字,之后用并查集和入度出度数来判断是否为欧拉通路即可
代码
#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <cstring>
using namespace std;
int trie[1212121][27],tot=1,fa[1212121],col[1212121],color,degree[1212121];
bool End[1212121];
char s1[20],s2[20],s[100];
int Find(int x) {
if(x==fa[x])
return x;
return fa[x]=Find(fa[x]);
}
void Union(int x,int y) {
int _x=Find(x),_y=Find(y);
if(_x!=_y)
fa[_x]=fa[_y];
}
int Insert(char *s) {
int len=strlen(s),p=1;
for(int i=0; i<len; i++) {
int ch=s[i]-'a';
if(!trie[p][ch])
trie[p][ch]=++tot;
p=trie[p][ch];
}
if(End[p])//单词已存在,返回编号
return col[p];
else {
End[p]=1;
col[p]=++color;//不存在则创建,这是单词到数字的索引
return col[p];
}
}
int main() {
for(int i=1; i<1212121; i++)
fa[i]=i;
while(gets(s)) {
if(s[0]=='\0')
break;
sscanf(s,"%s%s",s1,s2);
int a=Insert(s1),b=Insert(s2);//字符串转数字
Union(a,b);
degree[a]++;
degree[b]++;
}
int t=Find(1),num=0;
for(int i=1; i<=color; i++) {//判断欧拉通路
if(degree[i]%2)
num++;
if(num>2||Find(i)!=t) {
printf("Impossible\n");
return 0;
}
}
if(num==0||num==2)
printf("Possible\n");
else
printf("Impossible\n");
return 0;
}
POJ3764
题目大意:在边权树种,路径p的xor(异或)长度被定义为路径p上边权的xor,若一个路径有最大的xor长度,则该路径是xor最长路径,给定n个节点的边权树,找到xor最长的路径
思路:首先,任意节点对边权xor值为dx[u] xor dx[v],相同的部分会抵消,在这个前提下,用链式前向星剑突,DFS图,求解每个节点从树根到当前节点的xor路径值,在字典树中查找dx[i]的异或结果,求最大值,并将dx[i]插入字典树中
代码解释如图
代码
#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
int head[1212121],n,cnt,dx[1212121],trie[3300000][3],tot=1;
//字典树的空间必须足够大,为位数*数字个数
struct node {
int to,next,v;
} e[1212121];
void Add(int from,int to,int v) {//链式前向星
e[cnt].next=head[from];
e[cnt].to=to;
e[cnt].v=v;
head[from]=cnt++;
}
void DFS(int u,int f) {
for(int i=head[u]; i!=-1; i=e[i].next) {
int v=e[i].to,w=e[i].v;
if(v==f)
continue;
dx[v]=dx[u]^w;
DFS(v,u);
}
}
void Insert(int num) {
int p=1;
for(int i=30; i>=0; i--) {
bool k=num&(1<<i);//取这一位,由高到低
if(!trie[p][k])
trie[p][k]=++tot;
p=trie[p][k];
}
}
int Find(int num) {
int p=1,res=0;
for(int i=30; i>=0; i--) {
bool k=num&(1<<i);
if(trie[p][k^1]) {//走反路径,这样才能得到最大的异或值
res+=1<<i;
p=trie[p][k^1];
} else
p=trie[p][k];//如果没有就只能走原数字,因为走到根才是完整的数字
}
return res;
}
int main() {
while(~scanf("%d",&n)) {
memset(head,-1,sizeof(head));
memset(dx,0,sizeof(dx));
memset(trie,0,sizeof(trie));
tot=1;
cnt=0;
for(int i=0; i<n-1; i++) {
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
Add(a,b,c);
Add(b,a,c);
}
DFS(0,-1);
for(int i=0; i<=n-1; i++)
Insert(dx[i]);
int ans=0;
for(int i=0; i<=n-1; i++)
ans=max(ans,Find(dx[i]));
printf("%d\n",ans);
}
return 0;
}
Codeforces Round #779 (Div. 2) D2. 388535 (Hard Version)
题目大意:给出两个整数 l , r l,r l,r,给出一个长度为 r − l + 1 r-l+1 r−l+1的排列: [ l , l + 1 , … , r ] [l,l+1,\dots,r] [l,l+1,…,r],对每个数异或一个 x x x,给出异或之后的结果,求 x x x,保证l≥0
思路:对于异或问题,首先由二进制来考虑,设异或的结果为数组
b
b
b,原数组为
a
a
a,暴力的思路是枚举一个
x
x
x来求异或最大值或者最小值,并且分别判断是否与
r
,
l
r,l
r,l相等,为什么要这样判断?显然,
b
b
b中的数字各不相同,根据异或的性质,每个数异或上
x
x
x之后,还原的序列里的数字也各不相同,所以只需要满足异或的最大值与最小值对应相等即可
暴力枚举值域内的
x
x
x显然不行,这个时候可以利用异或的性质,枚举每个
b
i
⊕
l
b_i⊕l
bi⊕l,因为
b
i
=
a
i
⊕
x
b_i=a_i⊕x
bi=ai⊕x,
b
i
b_i
bi一定有一个
l
⊕
x
l⊕x
l⊕x,最后会剩下
x
x
x,异或式子为
a
i
⊕
x
⊕
b
i
⊕
l
=
l
a_i⊕x⊕b_i⊕l=l
ai⊕x⊕bi⊕l=l
代码
#include <bits/stdc++.h>
//#define int long long
using namespace std;
const int maxn=5e5+50;
int t,l,r,trie[maxn][2],tot,a[maxn];
void Insert(int x) {
int p=1;
for(int i=17; i>=0; i--) {
bool k=x&(1<<i);
if(!trie[p][k]) {
trie[p][k]=++tot;
trie[tot][0]=trie[tot][1]=0;//初始化,防止先前的记录干扰
}
p=trie[p][k];
}
}
int GetMax(int x) {
int p=1,res=0;
for(int i=17; i>=0; i--) {
bool k=x&(1<<i);
if(trie[p][k^1]) {//走反路径,这样才能得到最大的异或值
res+=1<<i;
p=trie[p][k^1];
} else
p=trie[p][k];//如果没有就只能走原数字,因为走到根才是完整的数字
}
return res;
}
int GetMin(int x) {
int p=1,res=0;
for(int i=17; i>=0; i--) {
bool k=x&(1<<i);
if(trie[p][k])//走当前路径,获得最小异或值
p=trie[p][k];
else {
res+=1<<i;//给这一个赋值
p=trie[p][k^1];
}
}
return res;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cin >>t;
while(t--) {
cin >>l>>r;
tot=1;
trie[1][0]=trie[1][1]=0;//注意初始化,全部清零会超时
for(int i=1; i<=r-l+1; i++) {
cin >>a[i];
Insert(a[i]);
}
for(int i=1; i<=r-l+1; i++)
if(GetMax(a[i]^l)==r&&GetMin(a[i]^l)==l) {//判断最大值和最小值
cout <<(a[i]^l)<<endl;
break;
}
}
return 0;
}
总结
字典树是一种高效的查找结构,但是,从最后一题便可得知,它需要大量的空间来存储信息。字典树实现起来较为简单,变化也很多
字典树的应用
- 字符串检索。事先存储字符串,查找一个字符串是否出现过,出现的频率和搜索引擎的热门查询
- 前缀统计。统计一个串所有前缀单词的个数,只需统计根到叶子路径上单词出现的个数,也可以判断一个单词是否为另一个的前缀
- 最长公共前缀,大量字符串存储在同一棵树上时,可以快速获得某些字符串的前缀,对所有字符串都建立字典树,最长公共前缀长度即所在节点的LCA的长度
- 排序。对字典树先序遍历输出的字符串即字典序排序结果
- 作为后缀树,AC自动机等的辅助结构