trie简介
一、trie是一种用于实现字符串快速检索的多叉树结构。可以类比普通链表或二叉树进行理解:普通链表的每个结点最多有1个后继结点、二叉树的每个结点有最多有2个后继结点(左右儿子),而每个trie结点可以有n个后继结点。n的取值由文本的字符串的组成决定,假设文本中只含小写或大写字母,则可令n等于26。每个trie的结点用于表示单个字符是否存在,通常将结点定义为指针类型,指针为空则表示当前结点表示的字符不存在,反之则存在。trie的结点中除了后继数组之外还可以存若干个数据,用于表示“当前字符是否为单词的结尾”、“有几个单词以当前字符为结尾”等信息。
链表、二叉树、trie树对比:
//链表
struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {} //构造方法
};
//二叉树
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {} //构造方法
};
//trie树结点
struct TrieNode{
public:
bool Isword; //表示“当前字符是否为单词的结尾”
TrieNode *next[26]; //26个后继结点
};
二、一棵trie树由一个不表示字符的根结点确定,根结点是插入,查询的起点。假设trie树用于储存由小写字母构成的若干个字符串,那么就可以用后继数组next是否为空表示对应字符是否存在:next[0],next[1],…,next[25]是否为空分别表示当前字符(假设当前字符存在)后面是否存在字符a,b,…,z。
三、将若干个字符串以根结点为起始插入trie树就实现了trie的创建。具体就是先让指针指向根结点,然后遍历插入的字符串,遍历到字符ch的时候,就判断当前结点的next[ch-‘a’]是否为空(是否存在),为空(不存在)就分配一个新节点,并将指针指向新结点。不为空(存在)就直接将指针指向next[ch-‘a’]。重复此操作直到遍历完字符串,则完成了这个字符串的插入。通常在插入完成后修改末尾结点的数据,用于表示“当前字符是否为单词的结尾”、“有几个单词以当前字符为结尾”等信息。
插入例子:
假设向trie树中加入单词app、apple、in、ink、inner、trie、true则trie树如下图所示,其中橙色结点代表该结点是单词的结尾。
插入函数写法:
//插入函数写法
void insert(string word) {
int len=word.size();
TrieNode *pos=root; //指针指向根结点
for(int i=0;i<len;i++){
int ch=word[i]-'a'; //获取当前字符对应“下标”
//不存在则创建新结点(存在则不需创建,直接让pos往下指即可)
if(pos->next[ch]==NULL) pos->next[ch]=new TrieNode();
pos=pos->next[ch]; //指针指向下一个结点
}
pos->Isword=true; //标记此结点为一个单词的末尾
}
四、查询(包括查单词、查前缀等)的做法和插入类似。(1)查前缀操作:先让指针指向根结点,然后遍历要查询的字符串,遍历到字符ch的时候,就判断当前结点的next[ch-‘a’]是否为空(是否存在),为空则代表不存在该单词,直接返回。若当前结点不为空则将指针指向下一个结点。直到将字符串遍历完,若中途没有空结点,则该字符串是trie树中某些单词的前缀。(2)查单词和查前缀的区别仅在于,查单词不仅要把要查的单词遍历完,还需要满足指针最后停留的结点代表一个单词的结尾。
拿上面的图来说,假如要查“ap”是否是trie树中某些单词的前缀,只需将“ap”遍历完并且中途没有空结点即可。但若要查“ap”是否是trie中的一个单词,则必须满足“p”处结点表示单词的结尾。
查单词、查前缀函数写法:
//查询字符串word是否是trie树中存在的一个单词
bool search(string word) {
int len=word.size();
TrieNode *pos=root; //指向根结点
for(int i=0;i<len;i++){
int ch=word[i]-'a';
if(!pos->next[ch]) return false; //不存在单词中的字符则单词不存在
pos=pos->next[ch]; //指向下一个结点
}
return pos->Isword; //遍历完单词后,若此结点是单词结点则单词存在
}
//查询prefix是否是trie中某些单词的前缀
bool startsWith(string prefix) {
int len=prefix.size();
TrieNode *pos=root;
for(int i=0;i<len;i++){
int ch=prefix[i]-'a';
if(!pos->next[ch]) return false; //中途有不存在的结点则说明prefix不是前缀
pos=pos->next[ch];
}
return true; //遍历完则表明prifix是前缀
}
leetcode模板题
题目链接:leetcode 208. 实现 Trie (前缀树)
题目描述
Trie(发音类似 “try”)或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补完和拼写检查。
请你实现 Trie 类:
Trie() 初始化前缀树对象。
void insert(String word) 向前缀树中插入字符串 word 。
boolean search(String word) 如果字符串 word 在前缀树中,返回 true(即,在检索之前已经插入);否则,返回 false 。
boolean startsWith(String prefix) 如果之前已经插入的字符串 word 的前缀之一为 prefix ,返回 true ;否则,返回 false 。
示例:
输入
[“Trie”, “insert”, “search”, “search”, “startsWith”, “insert”, “search”]
[[], [“apple”], [“apple”], [“app”], [“app”], [“app”], [“app”]]
输出
[null, null, true, false, true, null, true]
解释
Trie trie = new Trie();
trie.insert(“apple”);
trie.search(“apple”); // 返回 True
trie.search(“app”); // 返回 False
trie.startsWith(“app”); // 返回 True
trie.insert(“app”);
trie.search(“app”); // 返回 True
C++代码
struct TrieNode{
public:
bool Isword;
TrieNode *next[26];
};
class Trie {
public:
TrieNode *root=NULL;
Trie() {
root=new TrieNode();
for(int i=0;i<26;i++){
root->next[i]=NULL;
}
}
void insert(string word) {
int len=word.size();
TrieNode *pos=root;
for(int i=0;i<len;i++){
int ch=word[i]-'a';
if(pos->next[ch]==NULL) pos->next[ch]=new TrieNode();
pos=pos->next[ch];
}
pos->Isword=true;
}
bool search(string word) {
int len=word.size();
TrieNode *pos=root;
for(int i=0;i<len;i++){
int ch=word[i]-'a';
if(!pos->next[ch]) return false;
pos=pos->next[ch];
}
return pos->Isword;
}
bool startsWith(string prefix) {
int len=prefix.size();
TrieNode *pos=root;
for(int i=0;i<len;i++){
int ch=prefix[i]-'a';
if(!pos->next[ch]) return false;
pos=pos->next[ch];
}
return true;
}
};
Java代码
class TrieNode{
boolean isWord;
TrieNode [] next;
TrieNode(){
isWord=false;
next=new TrieNode[26];
}
}
class Trie {
TrieNode root;
public Trie() {
root=new TrieNode();
}
public void insert(String word) {
TrieNode pr=this.root;
int n=word.length();
for(int i=0;i<n;i++){
char c=word.charAt(i);
if(pr.next[c-'a']==null){
TrieNode node=new TrieNode();
pr.next[c-'a']=node;
}
pr=pr.next[c-'a'];
}
pr.isWord=true;
}
public boolean search(String word) {
TrieNode pr=this.root;
int n=word.length();
for(int i=0;i<n;i++){
char c=word.charAt(i);
if(pr.next[c-'a']==null)
return false;
pr=pr.next[c-'a'];
}
return pr.isWord;
}
public boolean startsWith(String prefix) {
TrieNode pr=this.root;
int n=prefix.length();
for(int i=0;i<n;i++){
char c=prefix.charAt(i);
if(pr.next[c-'a']==null)
return false;
pr=pr.next[c-'a'];
}
return true;
}
}
trie的数组实现方式
还是以存26个小写字母为例,定义二维数组trie[i][j]表示一个结点,其中j从0-25分别表示此结点代表的字符为a-z。而tire[i][j]的值相当于上面实现方法中的地址,令p=trie[i][j]就相当于pr=pr.next[c-‘a’],trie[][]数组初始为0,trie[i][j]为0说明此结点不存在。在创建的过程中,给trie[i][j]分配一个id值表示其“地址”,以实现在trie树上的遍历。同时定义end[]数组标记对应结点是否是单词的结尾(或有几个单词以当前结点为结尾),如end[id]为1表示“地址id”处的字符是一个单词的结尾,其中id按插入的顺序依次编号。
例子:假设向trie树中依次加入单词ab、bc、abc、abc、efg,则trie树如下图所示,其中橙色结点代表该结点是单词的结尾,end[id]表示id处的字符是几个单词的结尾。
插入函数写法:
int Isword[maxn];
int id=0; //节点编号
void Insert(char *s){
int len=strlen(s),p=0;
for(int i=0;i<len;i++){
//不存在则给结点分配一个编号id表示其“地址”
if(!trie[p][s[i]-'a']) trie[p][s[i]-'a']=++id;
p=trie[p][s[i]-'a']; //p指向新的节点
}
end[p]++; //记录以“地址p”为结尾的单词个数
}
牛客网trie树题目一
题目描述
题目链接:牛客网 前缀统计
给定N个字符串
S
1
,
S
2
,
.
.
.
,
S
N
S_{1},S_{2},...,S_{N}
S1,S2,...,SN,接下来进行
M
M
M次询问,每次询问给定一个字符串
T
T
T,求
S
1
∼
S
N
S_{1}∼S_{N}
S1∼SN中有多少个字符串是
T
T
T的前缀。输入字符串的总长度不超过
1
0
6
10^6
106,仅包含小写字母。
输入描述:
第一行两个整数
N
,
M
N,M
N,M。接下来
N
N
N行每行一个字符串
S
i
Si
Si。接下来M行每行一个字符串表示询问。
输出描述:
对于每个询问,输出一个整数表示答案
示例1
输入
3 2
ab
bc
abc
abc
efg
输出
2
0
分析:
题意是求trie树中有几个单词是字符串T的前缀。只需将单词都插入trie树,然后查询T的时候直接累加“路径”上的end[id]即可。此题的样例图就是上面那个>_<。
C++代码:
#include<cstdio>
#include<cstring>
const int maxn=1e6+100;
int trie[maxn][30];
int end[maxn];
int id=0; //节点编号
void Insert(char *s){
int len=strlen(s),p=0;
for(int i=0;i<len;i++){
if(!trie[p][s[i]-'a']) trie[p][s[i]-'a']=++id; //不存在则创建并给一个新的编号
p=trie[p][s[i]-'a']; //p指向新的节点
}
end[p]++; //以p结尾的单词数加1
}
int IsPrefix(char *s){
int len=strlen(s),p=0,sum=0;
for(int i=0;i<len;i++){
if(!trie[p][s[i]-'a']) return sum; //如果已经没有节点,直接返回sum
p=trie[p][s[i]-'a']; //p指向新的节点
sum+=end[p]; //加上以当前节点为结尾字母单词的个数
}
return sum;
}
int main(){
int N,M;
scanf("%d%d",&N,&M);
char S[maxn];
while(N--){
scanf("%s",&S);
Insert(S);
}
int res=0;
char T[maxn];
while(M--){
scanf("%s",&T);
printf("%d\n",IsPrefix(T));
}
return 0;
}
牛客网trie树题目二
题目描述
题目链接:The XOR Largest Pair
在给定的 N 个整数 A1,A2……AN 中选出两个进行 xor(异或)运算,得到的结果最大是多少?
输入格式
第一行输入一个整数 N。
第二行输入 N 个整数 A1~AN。
输出格式
输出一个整数表示答案。
数据范围
1
≤
N
≤
1
0
5
,
0
≤
A
i
<
2
31
1≤N≤10^5,0≤Ai<2^{31}
1≤N≤105,0≤Ai<231
输入样例:
3
1 2 3
输出样例:
3
分析:
可直接枚举两个数的异或值,选出最大的即可,但是此做法时间复杂度为 o ( n 2 ) o(n^2) o(n2),此数据规模下将会超时。
trie树优化:
问题转化:已知数
x
x
x和数列
a
1
,
.
.
.
,
a
n
a_{1},...,a_{n}
a1,...,an,如何找出数列中的一个数使其和
x
x
x的异或值最大?异或是“二进制表示下的按位操作”,可把数列
a
1
,
.
.
.
,
a
n
a_{1},...,a_{n}
a1,...,an按二进制表示下的形式从高位到低位依次插入trie树,再把
x
x
x当作要查询的数向下查询即可。熟悉异或运算的话就会知道0^1=1, 1^0=1 , 0^0=0 ,1^1=0。即对于0,1两个状态的异或,不同为1,相同为0。假如
x
x
x的当前位为1就选下一位表示0的路径,假如
x
x
x的当前位为0就选下一位表示1的路径,这样当前位就不会被消去,异或值中就会保留
2
i
2^i
2i,(
i
i
i是当前的位数),由于是把二进制从高位到低位插入trie树中,所以从根结点开始不断选择和当前位相异的路径走下去,必定可以得到最大值(从高位到低位选不同的路径可以保证尽量不让高位被抵消)。注意,若当前只有一个路径,就只能选择该路径走。
假如往trie树里插入1,3,7,10,13,14,15则得到如下图所示trie树:
假如在上面的trie树中分别查询哪个数和8(二进制为1000)、3(二进制为0011)的异或值最大,则查询路径即结果如下图所示(8和7异或可得最大值,3和13异或可得最大值):
C++代码:
#include<iostream>
#include<cstring>
using namespace std;
const int N=1e5+100;
int trie[30*N][2],a[N];
int id=0;
void Insert(int x){
int p=0;
for(int i=30;i>=0;i--){
int ch=x>>i&1; //获取x二进制的第i位值
if(!trie[p][ch]){
trie[p][ch]=++id; //如果没有结点则创建新结点
}
p=trie[p][ch];
}
}
//查询x和数组中某数所能产生的最大异或值
int search(int x){
int p=0,sum=0;
for(int i=30;i>=0;i--){
int ch=x>>i&1;
//如果存在与当前位不同的路径,就选这条路径走
if(trie[p][!ch]){
sum+=(1<<i); //累加没有被抵消的权值
p=trie[p][!ch];
}
else{
//由于权值被抵消,省略sum+=0
p=trie[p][ch];
}
}
return sum;
}
int main(){
int n;
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
//将数组中的数都插入trie
Insert(a[i]);
}
int res=0;
for(int i=1;i<=n;i++){
//查询a[i]和数组中某个数异或能够产生的最大异或值,并更新答案res
res=max(res,search(a[i]));
}
cout<<res<<endl;
return 0;
}
牛客网trie树题目三
题目描述
题目链接:The XOR-longest Path
给定一棵n个点的带权树,求树上最长的异或和路径。
输入描述:
第一行一个整数n,接下来n-1行每行三个整数u,v,w,表示u,v之间有一条长度为w的边。
输出描述:
输出一行一个整数,表示答案。
示例1
输入
4
1 2 3
2 3 4
2 4 6
输出
7
说明
最长的异或和路径是
1
→
2
→
3
1→2→3
1→2→3,它的长度是
3
⨁
4
=
7
3⨁4=7
3⨁4=7。
注意:结点下标从1开始到N。
注:x⨁y表示x与y按位异或。
备注:
对于100%的数据, 1 ≤ n ≤ 1 0 5 , 1 ≤ u , v ≤ n , 0 ≤ w < 2 31 1≤n≤10^5,1≤u,v≤n,0≤w<2^{31} 1≤n≤105,1≤u,v≤n,0≤w<231
trie解法:
- 此处存在一个规律:连接结点
x
x
x和结点
y
y
y的异或和路径的长度等于 “
x
x
x和根节点的异或和路径长度”与“
y
y
y和根节点的异或和路径长度”的异或值。
如下图中的例子:
结点6和结点7的异或和路径长度为 7 ^ 4 ^ 3 ,结点6和根结点1的异或和路径长度为7 ^ 4 ^ 6 ,结点7和根结点1的异或和路径长度为3 ^ 6 。故“结点6和根节点的异或和路径长度”与“结点7和根节点的异或和路径长度”的异或值为(7 ^ 4 ^ 6)^(3 ^ 6)。而 (7 ^ 4 ^ 6)^ (3 ^ 6)=7 ^ 4 ^ 3 。
熟悉异或的性质不难理解上述规律:由于两个相同的数相异或得0,而两个结点的到根结点的异或和路径相异或正好抵消了那段重合的异或值。
- 基于以上规律,可以得到解决方法:dfs获取每个结点与根结点的异或和路径长度,然后从这些路径长度中找出两个路径长度使其异或值最大即可。上一题已经给出了在一串数中找两个数的最大异或和,故此处只需将每个结点与根结点的异或和路径长度像上一题一样插入trie中,然后查询一遍即可。
C++代码:
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int N=1e5+100;
int trie[35*N][2],id=0;
void Insert(int x){
int p=0;
for(int i=30;i>=0;i--){
int ch=x>>i&1; //获取x二进制的第i位值
if(!trie[p][ch]){
trie[p][ch]=++id; //如果没有结点则创建新结点
}
p=trie[p][ch];
}
}
//查询x和数组中某数所能产生的最大异或值
int search(int x){
int p=0,sum=0;
for(int i=30;i>=0;i--){
int ch=x>>i&1;
//如果存在与当前位不同的路径,就选这条路径走
if(trie[p][!ch]){
sum+=(1<<i); //累加没有被抵消的权值
p=trie[p][!ch];
}
else{
//由于权值被抵消,省略sum+=0
p=trie[p][ch];
}
}
return sum;
}
//链式前向星存图
struct egde{
int u;
int v;
int w;
int next;
}e[2*N];
int head[N],vis[N],d[N],cnt=0;
void Insert(int u,int v,int w){
cnt++;
e[cnt].u=u;
e[cnt].v=v;
e[cnt].w=w;
e[cnt].next=head[u];
head[u]=cnt;
}
//dfs遍历的过程中获取每个结点与根结点异或和路径长度
void dfs(int u){
vis[u]=1;
for(int i=head[u];i>=0;i=e[i].next){
if(!vis[e[i].v]){
d[e[i].v]=d[u]^e[i].w;
dfs(e[i].v);
}
}
}
int main(){
memset(head,-1,sizeof(head));
memset(vis,0,sizeof(vis));
int n;
scanf("%d",&n);
int u,v,w;
for(int i=1;i<=n-1;i++){
scanf("%d%d%d",&u,&v,&w);
Insert(u,v,w);
Insert(v,u,w);
}
dfs(1);
for(int i=1;i<=n;i++) Insert(d[i]);
int res=0;
for(int i=1;i<=n;i++){
//查询d[i]和数组中某个数异或能够产生的最大异或值,并更新答案res
res=max(res,search(d[i]));
}
printf("%d\n",res);
return 0;
}