关于trie 其实字典树和以上两种算法有很大不同,但是hash由于其优秀的应用,导致有些字符串查找用hash也是可行的.
字典树中支持添加,查找,区间查询(可持久化字典树),而且在异或操作上有更加好的操作;
前置知识 树的基本构造;
入坑 字典树是通过动态建点,而形成的树,基本数组有两维, $tr[x][to]$ 中第一维存的是节点标号,而第二维存的是当字符为 $to$ 时通向的节点;
基本操作 我当时入门是学的是这道题 ;
给你一些初始字符串,询问,给你一个字符串,这个字符串在这个初始字符串中是否存在
当时使用hash写的,但是没过;
现在我们可以用字典树先存一下初始字符串,然后在树上匹配,单次时间复杂度 $O(n)$ ;
code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 #include <bits/stdc++.h> using namespace std ;int n,m,trie[300007 ][27 ],num[300007 ],sz;bool vis[300007 ];void build (char a[]) { int now=0 ; for (int i=0 ;i<strlen (a);i++){ if (!trie[now][a[i]-'a' ]) trie[now][a[i]-'a' ]=++sz; now=trie[now][a[i]-'a' ]; } num[now]++; } int exam (char a[]) { int now=0 ; for (int i=0 ;i<strlen (a);i++){ if (!trie[now][a[i]-'a' ]) return 0 ; now=trie[now][a[i]-'a' ]; } if (!num[now]) return 0 ; if (vis[now]) return 1 ; vis[now]=1 ; return 2 ; } int main () { scanf ("%d" ,&n); for (int i=1 ;i<=n;i++){ char a[60 ]; scanf ("%s" ,a); build(a); } scanf ("%d" ,&m); for (int i=1 ;i<=m;i++){ char a[60 ]; scanf ("%s" ,a); int x=exam(a); switch (x){ case 0 :{ printf ("WRONG\n" ); break ; } case 1 :{ printf ("REPEAT\n" ); break ; } case 2 :{ printf ("OK\n" ); break ; } } } }
另 我在hash中介绍了map,这里其实也可以用map存字符串,但是其时间复杂度比原来的多了一个 $logn$ ,写法虽然简单但是时间并不优秀;
code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include <bits/stdc++.h> using namespace std ;map <string ,int >p;int n,m;string s;int main () { scanf ("%d" ,&n); for (int i=1 ;i<=n;i++){ cin >>s; p[s]=1 ; } scanf ("%d" ,&m); for (int i=1 ;i<=m;i++){ cin >>s; if (p[s]==1 ){ puts ("OK" ),p[s]=2 ; }else if (p[s]==0 ){ puts ("WRONG" ); }else if (p[s]==2 ){ puts ("REPEAT" ); } } return 0 ; }
拓展 异或,是我们经常会见到的,但是如何高效的处理异或信息是一个让人头痛的事,而字典树为我们提供了策略.
0/1串树 $0/1$ 串树,常用来储存一个2进制数字,我们知道异或正是与二进制有关,那么我们是否可以找在字典树上操作序列呢?
显然是可以的,我们在一个节点,分别走0通向的节点和1通向的节点,那么贪心地操作,这样一定是异或对最大值,反之,都走0或者1可以有效地得到最小值,
例题1 最长异或路径 ,这个例题应该比较合适;
给定一棵 $n$ 个点的带权树,结点下标从 $1$ 开始到 $N$ 。寻找树中找两个结点,求最长的异或路径。
异或路径指的是指两个结点之间唯一路径上的所有边权的异或。
显然,我们可以将每个点到根root的异或和保存一下,然后将其加入字典树,现在我们要求的就是最大的异或数对,跟上面说的一样,我们只要从字典树顶端开始BFS,就可以得到最大异或数对.
code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 #include <bits/stdc++.h> #define maxn 100007 #define mp(x,y,z,w) (nd){x,y,z,w} using namespace std ;int n,head[maxn],dis[maxn],tr[maxn*32 ][2 ],ed[maxn*32 ],val[maxn*32 ];int tot,cent,ans[34 ],ol;struct node { int next,to,w; }edge[maxn<<2 ]; struct nd { int x,y,val,dep; }; template <typename type_of_scan>inline void scan (type_of_scan &x) { type_of_scan f=1 ;x=0 ;char s=getchar(); while (s<'0' ||s>'9' ){if (s=='-' )f=-1 ;s=getchar();} while (s>='0' &&s<='9' ){x=x*10 +s-'0' ;s=getchar();} x*=f; } template <typename top,typename ... tops>inline void scan (top &x,tops&... X) { scan(x),scan(X...); } inline void add (int u,int v,int w) { edge[++cent]=(node){head[u],v,w};head[u]=cent; edge[++cent]=(node){head[v],u,w};head[v]=cent; } void dfs (int x,int fa) { for (int i=head[x];i;i=edge[i].next){ int y=edge[i].to; if (y==fa) continue ; dis[y]=dis[x]^edge[i].w; dfs(y,x); } } void insert (int x,int id) { int now=0 ; for (int i=30 ;i>=0 ;i--){ int pos=!!(x&(1 <<i)); if (!tr[now][pos]) tr[now][pos]=++tot; now=tr[now][pos]; } ed[tot]=id,val[tot]=x; } queue <nd>q;int bfs () { q.push(mp(0 ,0 ,0 ,0 )); while (!q.empty()){ int x=q.front().x,y=q.front().y,d=q.front().dep; int val=q.front().val;q.pop(); if (d>30 ) return ans[d]; if (ans[d]>val) continue ; if (tr[x][0 ]&&tr[y][1 ]){ q.push(mp(tr[x][0 ],tr[y][1 ],val<<1 |1 ,d+1 )); ans[d+1 ]=max(ans[d+1 ],val<<1 |1 ); } if (tr[x][1 ]&&tr[y][0 ]){ if (x!=y) q.push(mp(tr[x][1 ],tr[y][0 ],val<<1 |1 ,d+1 )); ans[d+1 ]=max(ans[d+1 ],val<<1 |1 ); }else { if (!tr[x][0 ]||!tr[y][1 ]){ if (tr[x][0 ]&&tr[y][0 ]){ q.push(mp(tr[x][0 ],tr[y][0 ],val<<1 ,d+1 )); ans[d+1 ]=max(ans[d+1 ],val<<1 ); } if (tr[x][1 ]&&tr[y][1 ]){ q.push(mp(tr[x][1 ],tr[y][1 ],val<<1 ,d+1 )); ans[d+1 ]=max(ans[d+1 ],val<<1 ); } } } } } int main () { scan(n); for (int i=1 ,u,v,w;i<=n-1 ;i++) scan(u,v,w),add(u,v,w); dfs(1 ,0 ); for (int i=1 ;i<=n;i++) insert(dis[i],i),ol=max(ol,dis[i]); printf ("%d\n" ,max(ol,bfs())); return 0 ; }
例题2 CF888G Xor-MST
已知一个 $n$ 个节点的无向完全图,每个节点的编号为 $a_i$ , $i$ 与 $j$ 的边的权值是 $a_i$ ^ $a_j$ ,求该图的 $MST$ 的权值;
我们可以想一下 $kruskal$ 算法的过程,那么我们也可以每次寻找最小值,可以通过在trie上BFS得到;
值得注意的是,所有分叉点的个数为建边个数(去掉两点权值相同),那么其实直接寻找即可;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 #include <bits/stdc++.h> #define maxn 200007 #define mp(x,y,z,w) (node){x,y,z,w} #define ll long long using namespace std ;int n,m,a[maxn],tr[maxn*33 ][2 ],ed[maxn*33 ],tot;int fa[maxn];bool vis[maxn*33 ];vector <int >dep[34 ];struct node { int x,y,d,ans; }; template <typename type_of_scan>inline void scan (type_of_scan &x) { type_of_scan f=1 ;x=0 ;char s=getchar(); while (s<'0' ||s>'9' ){if (s=='-' )f=-1 ;s=getchar();} while (s>='0' &&s<='9' ){x=x*10 +s-'0' ;s=getchar();} x*=f; } template <typename type_of_print>inline void print (type_of_print x) { if (x>9 ) print(x/10 ); putchar (x%10 +'0' ); } void add (int x,int id) { int now=0 ,d=0 ; for (int i=30 ;i>=0 ;i--,d++){ int pos=(!(x&(1 <<i))); if (!tr[now][pos]) tr[now][pos]=++tot; if (tr[now][pos^1 ]&&!vis[now]) vis[now]=1 ,dep[d].push_back(now); now=tr[now][pos]; } if (ed[now]) fa[ed[now]]=id; ed[now]=id; } queue <node>q;int find (int now,int d,int &x,int &y) { int f1=tr[now][0 ],f2=tr[now][1 ]; while (!q.empty()) q.pop(); int ans[32 ]; memset (ans,127 ,sizeof ans);ans[d]=1 ; q.push((node){f1,f2,d,1 }); while (!q.empty()){ x=q.front().x,y=q.front().y; int dx=q.front().d,ans1=q.front().ans;q.pop(); if (ed[x]&&ed[y]){ x=ed[x],y=ed[y]; break ; } if (ans1>ans[dx]) continue ; if (tr[x][0 ]&&tr[y][0 ]) q.push(mp(tr[x][0 ],tr[y][0 ],dx+1 ,ans1<<1 )),ans[dx+1 ]=min(ans[dx+1 ],ans1<<1 ); if (tr[x][1 ]&&tr[y][1 ]) q.push(mp(tr[x][1 ],tr[y][1 ],dx+1 ,ans1<<1 )),ans[dx+1 ]=min(ans[dx+1 ],ans1<<1 ); else if (!tr[x][0 ]||!tr[y][0 ]){ if (tr[x][1 ]&&tr[y][0 ]) q.push(mp(tr[x][1 ],tr[y][0 ],dx+1 ,ans1<<1 |1 )),ans[dx+1 ]=min(ans[dx+1 ],ans1<<1 |1 ); if (tr[y][1 ]&&tr[x][0 ]) q.push(mp(tr[x][0 ],tr[y][1 ],dx+1 ,ans1<<1 |1 )),ans[dx+1 ]=min(ans[dx+1 ],ans1<<1 |1 ); } } return ans[30 ]; } int get (int x) {return fa[x]==x?x:fa[x]=get(fa[x]);}ll ans; int main () { scan(n); for (int i=1 ;i<=n;i++) fa[i]=i; for (int i=1 ;i<=n;i++){ scan(a[i]);add(a[i],i); } for (int i=30 ;i>=0 ;i--){ if (!dep[i].size()) continue ; for (int j=0 ;j<dep[i].size();j++){ int x,y,f; f=find(dep[i][j],i,x,y); if (get(x)==get(y)) continue ; fa[get(x)]=get(y);ans+=1l l*f; } } printf ("%lld" ,ans); }
可持久化串树 可持久化串树,即可在区间中查询的串树,与可持久数组有些类似,只是前者用trie,后者用主席树罢了.
建树时,我们可以再建一个节点,然后继承上一个节点的信息,然后再建一个新节点去保存自己的信息.
区间查询时,我们进入右端点的trie节点,为了限制左边界,我们可以在建图时将其序号标上,在搜索到小于左端点编号时跳过,去寻找另一个节点即可.
建树(0/1trie) 1 2 3 4 5 6 7 8 9 10 11 12 void build (int x,int last,int &f,int pos) { int now;now=f=++tot; for (int i=lim,ol=(x&(1 <<i))>>i;i>=0 ;i--,ol=(x&(1 <<i))>>i){ trie[now][0 ]=trie[last][0 ]; trie[now][1 ]=trie[last][1 ]; last=trie[last][ol]; trie[now][ol]=++tot; mark[tot]=pos; now=trie[now][ol]; } ending[now]=x; }
查询(0/1trie) 1 2 3 4 5 6 7 8 int dfs (int x,int f,int op) { int now=f; for (int i=lim,ol=(x&(1 <<i))>>i;i>=0 ;i--,ol=(x&(1 <<i))>>i){ if (trie[now][ol^1 ]&&mark[trie[now][ol^1 ]]>=op) now=trie[now][ol^1 ]; else now=trie[now][ol]; } return ending[now]; }
这里的查询是在一段区间中查询异或k的最大值;
例题1 最大异或和 ,模版题;
我们就按照上面的步骤即可;
code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 #include <bits/stdc++.h> #define maxn 1200007 using namespace std ;int n,m,trie[maxn*10 ][2 ],a[maxn],sum[maxn],tot;int ending[maxn*10 ],mark[maxn*10 ],lim,id[maxn];template <typename type_of_scan>inline void scan (type_of_scan &x) { type_of_scan f=1 ;x=0 ;char s=getchar(); while (s<'0' ||s>'9' ){if (s=='-' )f=-1 ;s=getchar();} while (s>='0' &&s<='9' ){x=x*10 +s-'0' ;s=getchar();} x*=f; } template <typename top,typename ... tops>inline void scan (top &x,tops&... X) { scan(x),scan(X...); } void build (int x,int last,int &f,int pos) { int now;now=f=++tot; for (int i=lim,ol=(x&(1 <<i))>>i;i>=0 ;i--,ol=(x&(1 <<i))>>i){ trie[now][0 ]=trie[last][0 ]; trie[now][1 ]=trie[last][1 ]; last=trie[last][ol]; trie[now][ol]=++tot; mark[tot]=pos; now=trie[now][ol]; } ending[now]=x; } int dfs (int x,int f,int op) { int now=f; for (int i=lim,ol=(x&(1 <<i))>>i;i>=0 ;i--,ol=(x&(1 <<i))>>i){ if (trie[now][ol^1 ]&&mark[trie[now][ol^1 ]]>=op) now=trie[now][ol^1 ]; else now=trie[now][ol]; } return ending[now]; } int main () { scan(n,m); for (int i=1 ;i<=n;i++){ scan(a[i]),sum[i]=sum[i-1 ]^a[i]; lim=max(sum[i],lim); } lim=(int )log2(1e7 )+1 ; for (int i=1 ;i<=n;i++) build(sum[i],id[i-1 ],id[i],i); for (int i=1 ,l,r,x;i<=m;i++){ char s=getchar(); while (s!='A' &&s!='Q' ) s=getchar(); if (s=='A' ) scan(x),n++,sum[n]=sum[n-1 ]^x,build(sum[n],id[n-1 ],id[n],n); else if (s=='Q' ){ scan(l,r,x); printf ("%d\n" ,(sum[n]^x)^dfs((sum[n]^x),id[r-1 ],l-1 )); } } return 0 ; }
例题2 异或粽子 ,可持久化trie查询区间最大异或值;
这道题的思路可以从 超级钢琴 中得到.
超级钢琴的思路是将权值处理出来,与区间信息一起保存在优先队列中,然后每次取出最大值,再更新左右区间即可;
而这道题与其不同的是,这里将权值处理出来的方式不同,这里运用可持久化trie,然后在区间查询最大异或值,其他的与超级钢琴几乎一致;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 #include <bits/stdc++.h> #define maxn 1000007 #define ll long long using namespace std ;ll n,k,trie[maxn*23 ][2 ],mark[maxn*23 ],val[maxn*23 ]; ll sum[maxn],ans,tot,lim=33 ,a[maxn],id[maxn]; struct node { ll val,l,r,pos,ori; }; priority_queue<node>q; template <typename type_of_scan>inline void scan (type_of_scan &x) { type_of_scan f=1 ;x=0 ;char s=getchar(); while (s<'0' ||s>'9' ){if (s=='-' )f=-1 ;s=getchar();} while (s>='0' &&s<='9' ){x=x*10 +s-'0' ;s=getchar();} x*=f; } template <typename top,typename ... tops>inline void scan (top &x,tops&... X) { scan(x),scan(X...); } bool operator <(node a,node b){ return a.val<b.val; } void build (ll x,ll last,ll &f,ll pos) { ll now;now=f=++tot; for (ll i=lim,ol=(x&(1 <<i))>>i;i>=0 ;i--,ol=(x&(1 <<i))>>i){ trie[now][0 ]=trie[last][0 ]; trie[now][1 ]=trie[last][1 ]; last=trie[last][ol]; trie[now][ol]=++tot; mark[tot]=pos; now=trie[now][ol]; } val[now]=x; } ll query (ll x,ll f,ll op,ll &pos) { ll now=f; for (ll i=lim,ol=(x&(1 <<i))>>i;i>=0 ;i--,ol=(x&(1 <<i))>>i){ if (trie[now][ol^1 ]&&mark[trie[now][ol^1 ]]>=op) now=trie[now][ol^1 ]; else now=trie[now][ol]; } return pos=mark[now],val[now]; } int main () { scan(n,k); for (ll i=1 ;i<=n;i++){ scan(a[i]),sum[i]=sum[i-1 ]^a[i]; build(sum[i],id[i-1 ],id[i],i); } for (ll i=1 ;i<=n;i++){ ll ol,pos; ol=query(sum[i-1 ],id[n],i,pos); q.push((node){ol^sum[i-1 ],i,n,pos,sum[i-1 ]}); } for (ll i=1 ;i<=k;i++){ if (q.empty()) continue ; node x=q.top();ans+=x.val;q.pop(); ll ol,pos; if (x.l<x.pos){ ol=query(x.ori,id[x.pos-1 ],x.l,pos); q.push((node){ol^x.ori,x.l,x.pos-1 ,pos,x.ori}); } if (x.pos<x.r){ ol=query(x.ori,id[x.r],x.pos+1 ,pos); q.push((node){ol^x.ori,x.pos+1 ,x.r,pos,x.ori}); } } printf ("%lld\n" ,ans); }
后记 这里只是总结了一下trie的用法,我见到的主要还是0/1trie,以后见到还会再加入;
TO BE CONTINUED