Trie树小结
说来惭愧,一个月前就写了trie的题,现在才想起来整理博客,中途我会隔一段时间在敲一下原来做过的题,能做到速度和质量后再来写一下我的一些心得吧;
Trie树:一种基本的数据结构;
用来实现字符串快速检索的一种多叉树,每个节点拥有多个字符指针,若在插入或检索中扫描到一个字符c,沿着当前指针走向指针所指的下一个节点;
1.初始化:只有一个根节点,字符指针均指向空;
2.插入:插入一个字符串s;
首先建一个指针P指向根节点,扫描每一个字符c,若P的c字符指针指向节点Q,那么令P=Q;
如果P的c字符指针指向空,那么新建一个节点Q,令P=Q
依次扫描,直至在字符串的末尾(当前的P节点)标记一下;
3,查询:查过一个字符串S是否在tire中出现过;
从根节点出发,扫描S的每一个字符c,如果当前指针P的c字符指针指向空,那么结束,S没有出现过;
如果扫描完S的每一个字符后,当前指针P被标记过,那么S出现过,否则没有出现过;
以上是trie的基本操作;
此类问题也常与前缀问题,异或问题结合,下面会接着讲;
加几道基础题:
题目大意:给定M个01串,N个01串,对于每一个N串,求满足有多少个M个串和它拥有相同的前缀,这个前缀长度必须等于N串和M串长度的的较小者;
思路:我们对M个串建一个trie树,用pass[i]表示经过节点i的M串有多少个,用c[i]表示用i结尾的字符串的个数;
那么对于query函数,我们只需要依次遍历N串的字符,用c[i]更新累加ans,只有两种情况:
1.如果指向空节点,返回答案ans;//因为当前字符串N的长度比路径上的字符串长,ans等于遍历的路径上的字符串总数;
2.这个串包括这个路径上的所有字符,返回ans+pass[i]-c[i];//画图理解一下;
#include<bits/stdc++.h> using namespace std; template<typename T>inline void read(T &x) { x=0;T f=1,ch=getchar(); while(!isdigit(ch)) {if(ch=='-') f=-1; ch=getchar();} while(isdigit(ch)) {x=(x<<1)+(x<<3)+(ch^48); ch=getchar();} x*=f; } int n,m,x,idx,a[500000],trie[500000][3],pass[500000],c[500000]; inline void insert(int k) { int p=0; for(int i=1;i<=k;i++) { int ch=a[i]; if(!trie[p][ch]) trie[p][ch]=++idx; p=trie[p][ch]; pass[p]++; } c[p]++; } inline int query(int k) { int p=0,ans=0; for(int i=1;i<=k;i++) { int ch=a[i]; if(!trie[p][ch]) return ans; p=trie[p][ch]; ans+=c[p]; } return ans+=pass[p]-c[p]; } int main() { read(n); read(m); for(int i=1;i<=n;i++) { read(x); for(int j=1;j<=x;j++) read(a[j]); insert(x); } for(int i=1;i<=m;i++) { read(x); for(int j=1;j<=x;j++) read(a[j]); cout<<query(x)<<endl; } return 0; }
2.L语言:
给定N个单词和M个句子,问这M个句子中包括这N个单词的最长前缀是多少;
思路:
我们对于N单词建一个trie树,对单词结尾打上标识;
对于每一个句子,我们用f[i]表示前i个字符是否可行,那么每次检索[i+1,L]//L指句子的长度;如果遍历到有标记的节点,更新f[i];
#include<bits/stdc++.h> using namespace std; template<typename T>inline void read(T &x) { x=0;T f=1,ch=getchar(); while(!isdigit(ch)) {if(ch=='-') f=-1; ch=getchar();} while(isdigit(ch)) {x=(x<<1)+(x<<3)+(ch^48); ch=getchar();} x*=f; } int n,m,idx,l,c[5000000],f[5000000],trie[5000000][26]; char ch[1500000]; inline void insert(char *s) { int p=0,l=strlen(s+1); for(int i=1;i<=l;i++) { int ch=s[i]-'a'; if(!trie[p][ch]) trie[p][ch]=++idx; p=trie[p][ch]; } c[p]=1; } inline int query(char *s,int st) { int p=0; for(int i=st;i<=l;i++) { int ch=s[i]-'a'; if(!trie[p][ch]) return 0; p=trie[p][ch]; if(c[p]) f[i]=1; } //return c[p]; } int main() { read(n); read(m); for(int i=1;i<=n;i++) { scanf("%s",ch+1); insert(ch); } for(int k=1;k<=m;k++) { scanf("%s",ch+1); l=strlen(ch+1); f[0]=1; for(int i=1;i<=l;i++) f[i]=0; for(int i=0;i<=l;i++) if(f[i]) query(ch,i+1); for(int i=l;i>=0;i--) if(f[i]) { cout<<i<<endl; break; } } return 0; }
题目大意:给出 N 个字符串,看是否有一个串 A 为串 B 的前缀。
因为我们在Trie树插入操作中已经对每一串字符串都打上了标记,
所以满足答案的关系只有两种,
一种是插入的串是之前的串的前缀,那么只要看在插入过程中是不是没有新增节点;
另一种是插入的串包括之前的串,也就是之前的串是插入的串的前缀,这个只要看看有没有走到打了标记的点就好了。
所以只需要在插入时判断即可;
请不要忘记清空trie;
#include<bits/stdc++.h> using namespace std; int T,n,m,idx,ans=0,trie[100050][25],cnt[100050]; char str[25]; template<typename T>inline void read(T &x) { x=0;T f=1,ch=getchar(); while(!isdigit(ch)) {if(ch=='-') f=-1;ch=getchar();} while(isdigit(ch)) {x=(x<<1)+(x<<3)+(ch^48);ch=getchar();} x*=f; } inline void clear() { for(int i=0;i<=idx;i++) { for(int j=0;j<=15;j++) { trie[i][j]=0,cnt[i]=0; } } } inline int insert(char str[]) { int p=0,l=strlen(str+1); int f1=0,f2=1; for(int i=1;i<=l;i++) { int ch=str[i]-'0'; if(!trie[p][ch]) trie[p][ch]=++idx,f2=0; p=trie[p][ch]; if(cnt[p]) f1=1; } cnt[p]=1; if(f1) return 1; if(f2) return 1; return 0; } int main() { read(T); while(T--) { clear(); ans=0;idx=0; read(n); for(int i=1;i<=n;i++) { scanf("%s",str+1); if(insert(str)) ans=1; } if(ans) cout<<"NO"<<endl; else cout<<"YES"<<endl; } return 0; }
以下是算法进阶指南上的题目:
1.前缀统计
题目描述:
给定N个字符串S1,S2…SN,接下来进行M次询问,每次询问给定一个字符串T,求S1~SN中有多少个字符串是T的前缀。
输入格式
第一行输入两个整数N,M。
接下来N行每行输入一个字符串Si。
接下来M行每行一个字符串T用以询问。
输出格式
对于每个询问,输出一个整数表示答案。
每个答案占一行。
思路:
我们将N个字符串插入trie树中,但是我们为了处理重复现象,我们将每个节点记录为有多少个字符串的在这里结尾,记录个数cnt,而不能只做结尾标记;
检索字符串T,累加路径上的cnt即可;
#include<bits/stdc++.h> using namespace std; #define N 1000010 int idx,n,m; int tire[N][26],cnt[N]; string s; template<typename T>inline void read(T &x) { x=0;T f=1,ch=getchar(); while(!isdigit(ch)) {if(ch=='-') f=-1; ch=getchar();} while(isdigit(ch)) {x=(x<<1)+(x<<3)+(ch^48); ch=getchar();} x*=f; } inline void insert(string s) { int p=0; for(int i=0;i<s.size();i++) { int ch=s[i]-'a'; if(tire[p][ch]==0) tire[p][ch]=++idx; p=tire[p][ch]; } cnt[p]++; } inline int query(string s) { int p=0,res=0; for(int i=0;i<s.size();i++) { p=tire[p][s[i]-'a']; if(p==0) return res; res+=cnt[p]; } return res; } int main() { read(n);read(m); for(int i=0;i<n;i++) { cin>>s; insert(s); } for(int i=0;i<m;i++) { cin>>s; cout<<query(s)<<endl; } return 0; }
题目描述:
在给定的N个整数A1,A2……ANA1,A2……AN中选出两个进行xor(异或)运算,得到的结果最大是多少?
输入格式
第一行输入一个整数N。
第二行输入N个整数A1A1~ANAN。
输出格式
输出一个整数表示答案。
数据范围
1≤N≤105
0≤Ai<231
思路:
枚举是行不通了;
根据XOR的性质是“相同为0,不同为1”,我们再选两个数异或时尽量选择每位不同的,尽可能的让结果大;
所以我们将每个数看做一个二进制数插入trie树中;
接下来对应每一个Ai,我们进行检索;
怎么检索呢,我们从根节点出发,对于当前Ai的j位,我们每次都尝试沿着与j位上数字的不同的指针向下检索,(这样答案尽可能大),
如果与其不同的指针指向空,那么我么只好沿着相同的走;
这样我们就找到了与当前Ai做XOR最大的值了;
#include<bits/stdc++.h> using namespace std; #define N 1000100 #define M 3000000 int n,a[N],tire[M][2],idx; template<typename T>inline void read(T &x) { x=0;T f=1,ch=getchar(); while(!isdigit(ch)) {if(ch=='-') f=-1; ch=getchar();} while(isdigit(ch)) {x=x*10+ch-'0'; ch=getchar();} x*=f; } inline void insert(int x) { int p=0; for(int i=31;~i;i--) { int ch=x>>i&1; if(tire[p][ch]==0) tire[p][ch]=++idx; p=tire[p][ch]; } } inline int query(int x) { int p=0,res=0; for(int i=31;~i;i--) { int ch=x>>i&1; if(tire[p][ch^1]) { p=tire[p][ch^1]; res+=1<<i; } else p=tire[p][ch]; } return res; } int main() { read(n); for(int i=1;i<=n;i++) { read(a[i]); insert(a[i]); } int ans=0; for(int i=1;i<=n;i++) ans=max(ans,query(a[i])); cout<<ans<<endl; }
题目描述:
给定一个树,树上的边都具有权值。
树中一条路径的异或长度被定义为路径上所有边的权值的异或和:
⊕ 为异或符号。
给定上述的具有n个节点的树,你能找到异或长度最大的路径吗?
输入格式
第一行包含整数n,表示树的节点数目。
接下来n-1行,每行包括三个整数u,v,w,表示节点u和节点v之间有一条边权重为w。
输出格式
输出一个整数,表示异或长度最大的路径的最大异或和。
数据范围
1≤n≤100000
0≤u,v<n
0≤w<231
思路:
设D[x]为根节点到x的路径上所有边权的XOR值;
那么有:D[x]=D[father[x]]xorWeight(x,father(x));
根据上式,我们可以先dfs求出当前所有节点的D[x]值,那么x到y的路径就是D[x]xorD[y],(即使没有经过根节点,那么重复的相同路径异或后为0,所以将相当于没有经过);
所以问题转化:
从D[1]~D[N]中选择两个使得XOR值最大,和上题类似;
同样使用trie树快速求解:
但是洛谷和Acwing的根节点不同,以下是Acwing的AC代码;
#include<bits/stdc++.h> using namespace std; #define N 500010 #define M 3000010 template<typename T>inline void read(T &x) { x=0;T f=1,ch=getchar(); while(!isdigit(ch)) {if(ch=='-') f=-1; ch=getchar();} while(isdigit(ch)) {x=x*10+ch-'0'; ch=getchar();} x*=f; } int n,x,y,z,tot,idx,lin[N],d[M],tire[M][2]; struct gg{ int y,next,v; }a[N<<1]; inline void add(int x,int y,int v) { a[++tot].y=y; a[tot].next=lin[x]; a[tot].v=v; lin[x]=tot; } inline void dfs(int x,int fa,int sum) { d[x]=sum; for(int i=lin[x];i;i=a[i].next) { int y=a[i].y; if(y!=fa) { dfs(y,x,sum^a[i].v); } } } inline void insert(int x) { int p=0; for(int i=31;~i;i--) { int ch=x>>i&1; if(tire[p][ch]==0) tire[p][ch]=++idx; p=tire[p][ch]; } } inline int query(int x) { int p=0,res=0; for(int i=31;~i;i--) { int ch=x>>i&1; if(tire[p][ch^1]) { p=tire[p][ch^1]; res+=1<<i; } else p=tire[p][ch]; } return res; } int main() { read(n); for(int i=1;i<n;i++) { read(x);read(y);read(z); add(x,y,z); add(y,x,z); } dfs(0,-1,0); for(int i=1;i<=n;i++) { insert(d[i]); } int ans=0; for(int i=1;i<=n;i++) ans=max(ans,query(d[i])); printf("%d\n",ans); return 0; }
ok撒~;(^_−)☆