TRIE:
在计算机科学中,Trie,又称前缀树或字典树,是一种有序树状的数据结构,用于保存关联数组,其中的键通常是字符串。——百度百科
自我理解:
trie树,是一种处理字符串前缀的数据结构,通常会有N*Len个节点,每个节点又引申出|S|个子节点指针,相当于一个很多叉的树,(甚至往往每个点叉的个数比高度还多)我们可以O(n)把待处理的字符串“挂到”trie上,最后统一查询,或者边挂边查。
可以发现,每个节点到根节点的路径就是一个前缀。
为什么要用字典树?
我们处理前缀问题的时候,往往需要求前缀的最值问题,公共前缀等等。朴素的做法都是要一个一个枚举,而我们把这些字符串集中到一个树上,通过公共前缀共用节点的特点,可以巧妙地不经过一一比较,就可以判断。
例如:
1.所有字符串LCP问题,朴素做法要处理hash,再在每个字符串上二分。logL* N,并且不能保证完全的正确性。毕竟有误差可能性。
通过trie树,相同的前缀已经被我们集中到了一起,我们只需要从根节点开始,一直找t[t[u].son].v==n的son节点,直到找不到为止,避免了对每个字符串进行操作的O(n)。
虽然预处理复杂度NlogL,但是查找的复杂度只有|S|*L,很少了。对于后续处理来说,预处理复杂度算不了什么。
2.两两字符串LCP问题:见例题:JZOJ 3126【GDKOI2013选拔】大LCP
但是,缺点很明显,trie的空间要更大,N太长就不行了。
需要的东西:
1.struct:son[28](如果是不分大小写的字典树),vis(该节点被访问过几次,或者:已经有多少个串拥有该节点代表的前缀,用于求LCP),num(编号为num的串结尾在这里,用于dfs确定字符串的字典序)2.insert:(洛谷2412)
void insert(char b[],int id) { int len=strlen(b); int u=0; for(int i=0;i<=len-1;i++){ if(!t[u].son[tol(b[i])]) { t[u].son[tol(b[i])]=++tot; u=tot; } else u=t[u].son[tol(b[i])]; } t[u].num=id; }
id:字符串编号,注意每次从根节点0开始插入。第一个有实际意义的点必须从1开始,根什么都不代表,只有指针。但是有编号0。
3.dfs
void dfs(int x){ if(t[x].num) ran[t[x].num]=++cnt; for(int i=1;i<=26;i++){ if(t[x].son[i]) dfs(t[x].son[i]); } }
确定所有插到trie上的字符串的字典序。
4.查找字符串,就直接找。
如果这个节点没有son[x[i]]这个出边,则返回没有;否则继续找,直到x[]找到底,判断这个点num是否为0,0返回没有,非0返回有。
5.查询任意两个字符串的LCP:两字符串对应的末尾节点,求LCA的深度,就是LCP
应用例题:(也有不是处理字符串的)
T1:JZOJ 3126【GDKOI2013选拔】大LCP
Description
LCP就是传说中的最长公共前缀,至于为什么要加上一个大字,那是因为…你会知道的。
首先,求LCP就要有字符串。既然那么需要它们,那就给出n个字符串好了。
于是你需要回答询问大LCP,询问给出一个k,你需要求出前k个字符串中两两的LCP最大值是多少,这就是传说中的大LCP。
Input
第一行一个整数N,Q,分别表示字符串个数和询问次数。
接下来N行,每行一个字符串。
再Q行,每行一个正整数k。
对于100%的数据,字符串总长度不超过10^6,1<=N,Q<=10^5.
分析:
这个题可以显著地体现trie求LCP的霸气所在。狂虐hash。
我们要是先都插入trie再处理询问,我怎么知道哪些是前k个产生的贡献?
询问是不强制在线的,所以把k从小到大排个序。按顺序插入,到了一个询问就输出正在更新的mx即可。
每次插入,直到到了一个要建新节点之前的所有经过的点,就是这个字符串与之前所有插入过的字符串的LCP长度。
例如叫做i字符串,都相当于是一个与1~i-1字符串进行LCP,直接O(1)带走啊。
相比较于hash,就可怜多了,必须n^2枚举字符串对,再二分LCP,n^2logL,哭死。
trie直接O(n),边插边查,复杂度大大下降。
前缀集合,trie确实优秀。
T2:poj2001
给定若干字符串,对于每个字符串求出一个最短前缀,使得这个前缀不是任何其他字符串的前缀。
分析:直接都挂上去,记录vis标记(见开头)。每个字符串按图索骥,直到某个点vis为1
T3:bzoj2251
给定一个长度为N的01串,要求按照字典序输出所有出现次数大于一次的子串的出现次数。
N<=3000。
分析:这个题有点想法。
我们知道,所有前缀的所有后缀就是所有子串,但是我们不能挂前缀啊,后缀怎么处理??
trie是处理前缀的。
不过还有一句话:所有后缀的所有前缀就是所有子串!!所以我们挂所有后缀。
节点标记vis,最后dfs按先0后1的顺序找到所有vis大于1的就行了。
为什么呢?因为vis=1,说明挂上的字符串中有一个从该点到根的前缀,而vis>1就说明有多个。
而我们挂上去的是后缀,有一个前缀,就有一个子串,有多个相同的前缀,就有多个相同的子串。等价转化。
#include<cstdio> #include<algorithm> #include<cstring> #include<cmath> #include<iostream> using namespace std; const int N=3000+5; struct trie{ int son[2]; int v; }t[N*N/2]; int tot; int n,len; char a[N]; char b[N]; void add(char x[],int l){ //cout<<x<<endl; int u=0; for(int i=0;i<l;i++){ int b=x[i]-'0'; if(t[u].son[b]){ u=t[u].son[b]; } else{ t[u].son[b]=++tot;u=tot; } t[u].v++; } } void dfs(int x){ if(t[x].v>1) printf("%d ",t[x].v); if(t[x].son[0]) dfs(t[x].son[0]); if(t[x].son[1]) dfs(t[x].son[1]); } int main() { scanf("%d",&n); scanf("%s",a+1); for(int i=n;i>=1;i--) { for(int j=i;j<=n;j++){ b[j-i]=a[j]; } add(b,strlen(b)); //if(i!=1) memset(b,0,sizeof b); } dfs(0); return 0; }
T4:洛谷P4551
题目描述
给定一棵 n 个点的带权树,结点下标从 1 开始到 N 。寻找树中找两个结点,求最长的异或路径。
异或路径指的是指两个结点之间唯一路径上的所有边权的异或。
数据范围
1≤n≤100000; 0<u,v≤n; 0≤w<2^31
分析:
这个题就比较考验洞察能力了。
因为异或运算满足交换律,结合律。所以我们可以求出每个点到根节点路径上的异或和dis[i]。
这样,我们要把所有的dis(二进制位)从高位到低位,挂到trie上。
对于节点i,先插入,再查询,将dis[i]高位补0对齐31位,从trie上往下找,每次先找有没有相反的,dis[i]这一位是0,找有没有1,反之找0
如果没有相反的,只能进入相同的了,然后指针后移,继续进行这个操作。
因为是从高位开始匹配,所以肯定尝试找在高位能异或出来1的可能性。
仍然利用前缀集合起来的性质,避免了枚举点对,O(n)扫一遍就好了。
#include<bits/stdc++.h> #define int long long using namespace std; typedef long long ui; const int N=100000+10; const int M=33; int ch[33*N][2]; int tot=1; ui dis[N]; struct node{ int nxt,to; ui val; }bian[4*N]; int hd[N]; int cnt; int n; ui ans,sum; void add(int x,int y,ui z) { bian[++cnt].nxt=hd[x]; bian[cnt].to=y; bian[cnt].val=z; hd[x]=cnt; } void dfs(int x,ui dist,int fa) { dis[x]=dist; for(int i=hd[x];i;i=bian[i].nxt) { int y=bian[i].to; if(y==fa) continue; if(x!=1) dfs(y,dist^bian[i].val,x); else dfs(y,bian[i].val,x); } } ui work(ui x) { ui st=31; ui sum=0; int now=1; while(st) { int kk=((unsigned int)x&((unsigned int)1<<st-1))>>(st-1); if(ch[now][!kk]) { now=ch[now][!kk],sum=sum+((unsigned int)1<<st-1); } else { now=ch[now][kk]; } st--; } return sum; } void puts(ui x) { ui st=31; int now=1; while(st) { int kk=((unsigned int)x&((unsigned int)1<<st-1))>>(st-1); if(ch[now][kk]) now=ch[now][kk]; else ch[now][kk]=++tot,now=tot; st--; } } signed main() { scanf("%lld",&n); int x,y,z; for(int i=1;i<=n-1;i++) { scanf("%lld%lld%lld",&x,&y,&z); add(x,y,z); add(y,x,z); } dfs(1,0,-1); for(int i=1;i<=n;i++) ans=max(ans,dis[i]);//warning!! puts(dis[2]); for(int i=3;i<=n;i++) { ans=max(ans,work(dis[i])); puts(dis[i]); } printf("%lld",ans); return 0; }