一、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 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
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;
}
};
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;
}
}
还是以存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”为结尾的单词个数
}
题目链接:牛客网 前缀统计
给定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]即可。此题的样例图就是上面那个>_<。
#include
#include
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;
}
题目链接: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异或可得最大值):
#include
#include
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;
}
题目链接: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
#include
#include
#include
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;
}