Trie树学习总结

Trie

简要说明

  • \(Trie\) 树在多个串的检索中十分实用,也是自动机的基础.
  • 它可以将若干个字符串组合成一颗树,不少题可以建好 \(Trie\) 树后在上面树形 \(dp\) .
  • 另外一个应用是解决最大异或值的问题,将数看成 \(01\) 串,贪心地在 \(Trie\) 树上走.

题目

Phone List

  • 题意:给若干数字串,问有没有两个串满足一个串是另一个串的前缀.
  • 将读入的串一个个插入 \(Trie\) 树中,不难发现串 \(A\) 是串 \(B\) 的前缀,只有两种情况.
  • 一种情况是串 \(A\) 先被插入,那么在插入串 \(B\) 时,一定会经过 \(A\) 的单词节点.
  • 另一种情况是串 \(B\) 先被插入,那么在插入串 \(A\) 时,都是沿着 \(B\) 的边走的,并没有新建边.
  • 一边插入,一边判断这两种情况是否出现即可.
  • 另一个简单而有效的做法是\(hash\).将出现过的前缀全部用 \(vis\) 数组标记即可.
View code

#include"bits/stdc++.h"
using namespace std;
typedef long long LoveLive;
inline int read()
{
    int out=0,fh=1;
    char jp=getchar();
    while ((jp>'9'||jp<'0')&&jp!='-')
        jp=getchar();
    if (jp=='-')
        {
            fh=-1;
            jp=getchar();
        }
    while (jp>='0'&&jp<='9')
        {
            out=out*10+jp-'0';
            jp=getchar();
        }
    return out*fh;
}
const int MAXN=5e5+10;
const int Siz=15;
int tcnt=0;
struct Trie{
    inline int idx(char c)
        {
            return c-'0';
        }
    int ch[MAXN][Siz];
    int val[MAXN];
    int cnt;
    void init()
        {
            memset(ch,0,sizeof ch);
            cnt=0;
        }
    int ins(char *s)
        {
            int n=strlen(s);
            int u=0;
            int f=0;
            for(int i=0;i
   
   

   
   

The XOR Largest Pair

  • 题意:给若干个整数,求出两两异或能得到的最大值.
  • 使用 \(Trie\) 树,将每个数看做一个长度为 \(32\) 的字符串插入 \(Trie\) 树中.
  • 插入完毕后,枚举一个数,在 \(Trie\) 树中往下走,尽量使每步的 \(0/1\) 和枚举的数都不一样.
  • 这样贪心取即可求出答案.
View code

#include"bits/stdc++.h"
using namespace std;
typedef long long LoveLive;
inline int read()
{
    int out=0,fh=1;
    char jp=getchar();
    while ((jp>'9'||jp<'0')&&jp!='-')
        jp=getchar();
    if (jp=='-')
        {
            fh=-1;
            jp=getchar();
        }
    while (jp>='0'&&jp<='9')
        {
            out=out*10+jp-'0';
            jp=getchar();
        }
    return out*fh;
}
const int MAXN=1e6+10;
const int MAXL=31;
struct Trie{
    int idx;
    int ch[10*MAXN][2];
    Trie()
        {
            idx=0;
            memset(ch,0,sizeof ch);
        }
    void ins(int x)
        {
            int u=0;
            for(int i=MAXL;i>=0;--i)
                {
                    int k=(x>>i)&1;
                    if(!ch[u][k])
                        ch[u][k]=++idx;
                    u=ch[u][k];
                }
        }
    int maxxor(int x)
        {
            int u=0;
            int res=0;
            for(int i=MAXL;i>=0;--i)
                {
                    int k=(x>>i)&1;
                    if(ch[u][k^1])
                        {
                            res<<=1;
                            res+=1;
                            u=ch[u][k^1];       
                        }
                    else
                        {
                            res<<=1;
                            u=ch[u][k];
                        }   
                }
            return res;
        }
}T;
int a[MAXN];
int main()
{
    int n=read();
    for(int i=1;i<=n;++i)
        {
            a[i]=read();
            T.ins(a[i]);
        }
    int ans=0;
    for(int i=1;i<=n;++i)
        ans=max(ans,T.maxxor(a[i]));
    printf("%d\n",ans);
    return 0;
}

Nikitosh 和异或

  • 题意:给出 \(n\) 个整数,选出两个严格不相交的区间 \(A=[l_1,r_1],B=[l_2,r_2]\) ,使得 \(A\) 的异或和加上 \(B\) 的异或和最大,求出这个最大值.
  • 若记录前缀异或和 \(s\) ,根据异或的性质,\(x \ xor\ x =0,\)\(a[l_1]\ xor\ a[l_1+1]\ xor \ ......a[r_1]=s[r_1]\ xor \ s[l_1-1].\)
  • 可以记\(l[i]\)表示在\(i\)左侧选出区间的最大异或值,\(r[i]\)表示在\(i\)右侧选出区间的最大异或值.
  • 将前/后缀依次插入 \(Trie\) 中,找出最大异或和即可.
View code

#include"bits/stdc++.h"
using namespace std;
typedef long long LoveLive;
inline int read()
{
    int out=0,fh=1;
    char jp=getchar();
    while ((jp>'9'||jp<'0')&&jp!='-')
        jp=getchar();
    if (jp=='-')
        {
            fh=-1;
            jp=getchar();
        }
    while (jp>='0'&&jp<='9')
        {
            out=out*10+jp-'0';
            jp=getchar();
        }
    return out*fh;
}
const int MAXN=1e6+10;
const int MAXL=31;
struct Trie{
    int idx;
    int ch[10*MAXN][2];
    void ins(int x)
        {
            int u=0;
            for(int i=MAXL;i>=0;--i)
                {
                    int k=(x>>i)&1;
                    if(!ch[u][k])
                        ch[u][k]=++idx;
                    u=ch[u][k];
                }
        }
    int maxxor(int x)
        {
            int u=0;
            int res=0;
            for(int i=MAXL;i>=0;--i)
                {
                    int k=(x>>i)&1;
                    if(ch[u][k^1])
                        {
                            res<<=1;
                            res+=1;
                            u=ch[u][k^1];       
                        }
                    else
                        {
                            res<<=1;
                            u=ch[u][k];
                        }   
                }
            return res;
        }
    void init()
        {
            idx=0;
            memset(ch,0,sizeof ch);
            ins(0);//避免1个数求不出的情况 
        }
    Trie()
        {
            init();
        }
}T;
int a[MAXN],n;
int l[MAXN],r[MAXN];//i两侧的最大值 
int main()
{
    n=read();
    int cur=0;
    for(int i=1;i<=n;++i)
        {
            a[i]=read();
            cur^=a[i];
            T.ins(cur);
            l[i]=max(l[i-1],T.maxxor(cur));
        }
    cur=0;
    T.init();
    for(int i=n;i>=1;--i)
        {
            cur^=a[i];
            T.ins(cur);
            r[i]=max(r[i+1],T.maxxor(cur));
        }
    int ans=0;
    for(int i=1;i
   
   

   
   

L 语言

  • 题意:给出若干个单词和若干篇文章.对于每篇文章,求出它能被完全匹配的最大前缀长度.
  • 将单词全部插入 \(Trie\) 树中.用\(f[i]\)表示前缀\(1\)\(i\) 能否被单词完全匹配.
  • 匹配文章时,应先连好失配边(所以这其实是一个 \(AC\) 自动机的题对吧).
  • 若当前匹配到了单词结点,且这个单词的开头的前面都能被完全匹配,显然这个位置的前缀可以被完全匹配,更新答案.
  • 在插入单词的时候将权值 \(val\) 设置为单词长度记录下来即可.
View code

#include"bits/stdc++.h"
using namespace std;
typedef long long LoveLive;
inline int read()
{
    int out=0,fh=1;
    char jp=getchar();
    while ((jp>'9'||jp<'0')&&jp!='-')
        jp=getchar();
    if (jp=='-')
        {
            fh=-1;
            jp=getchar();
        }
    while (jp>='0'&&jp<='9')
        {
            out=out*10+jp-'0';
            jp=getchar();
        }
    return out*fh;
}
const int MAXN=1e6+10;
const int Siz=26;
char buf[MAXN];
struct Trie{
    int ch[1000][Siz];
    int val[1000];
    int fail[MAXN];
    int idx;
    int f[MAXN];
    Trie()
        {
            memset(ch,0,sizeof ch);
            memset(fail,0,sizeof fail);
            idx=0;
        }
    void ins(char *s)
        {
            int n=strlen(s);
            int u=0;
            for(int i=0;i
   
   
    
     q;
            q.push(0);
            while(!q.empty())   
                {
                    int u=q.front();
                    q.pop();
                    for(int i=0;i
    
    

    
    
   
   

Secret Messages

  • 题意:给出 \(N\) 个信息串, \(M\) 个密码串.对于每个密码串,回答有几个信息串与它满足一个是另一个的前缀.
  • 将信息串插入 \(Trie\) 树中,记录子树大小.询问时,是密码串前缀的,在搜索过程中会经过,密码串是它的前缀的,到最后一个字符处加上子树大小即可.
View code

#include"bits/stdc++.h"
using namespace std;
typedef long long LoveLive;
inline int read()
{
    int out=0,fh=1;
    char jp=getchar();
    while ((jp>'9'||jp<'0')&&jp!='-')
        jp=getchar();
    if (jp=='-')
        {
            fh=-1;
            jp=getchar();
        }
    while (jp>='0'&&jp<='9')
        {
            out=out*10+jp-'0';
            jp=getchar();
        }
    return out*fh;
}
const int MAXN=5e5+10;
struct Trie{
    int idx;
    int ch[MAXN][2];
    int siz[MAXN];//计算自身的子树大小 
    int val[MAXN];
    Trie()
        {
            idx=0;
            memset(ch,0,sizeof ch);
            memset(siz,0,sizeof siz);
            memset(val,0,sizeof val);
        }
    void ins(int n)
        {
            int u=0;
            for(int i=1;i<=n;++i)
                {
                    int k=read();
                    if(!ch[u][k])
                        ch[u][k]=++idx;
                    u=ch[u][k];
                }
            ++val[u];
        }
    void dfs(int u)
        {
            siz[u]=val[u];
            for(int i=0;i<=1;++i)
                if(ch[u][i])
                    {
                        dfs(ch[u][i]);
                        siz[u]+=siz[ch[u][i]];
                    }
        }
    int solve(int n)
        {
            int u=0,res=0,flag=0;
            for(int i=1;i<=n;++i)
                {
                    int k=read();
                    if(!ch[u][k])
                        flag=1;
                    if(flag)
                        continue;
                    u=ch[u][k];
                    if(i!=n)
                        res+=val[u];
                    else
                        res+=siz[u];
                }
            return res;
        }
}T;
int main()
{
    int n=read(),m=read();
    for(int i=1;i<=n;++i)
        {
            int len=read();
            T.ins(len);         
        }
    T.dfs(0);
    for(int i=1;i<=m;++i)
        {
            int len=read();
            int ans=T.solve(len);
            printf("%d\n",ans);
        }
    return 0;
}

背单词

  • 题意:给你 \(n\) 个字符串,不同的排列有不同的代价,代价按照如下方式计算(字符串 \(s\) 的位置为 \(x\) ):

    • 1.排在 \(s\) 后面的字符串有 \(s\) 的后缀,则代价为$ n^2$ ;

    • 2.排在 \(s\) 前面的字符串有 \(s\) 的后缀,且没有排在 \(s\) 后面的 \(s\) 的后缀,则代价为 \(x-y\)\(y\) 为最后一个与 \(s\) 不相等的后缀的位置);

    • 3.\(s\) 没有后缀,则代价为\(x\)

    求最小代价和。

  • 将所有字符串翻转插入一颗 \(Trie\) 树中,则关于后缀的问题全部转化为前缀.

  • 容易发现,我们一定可以避免 \(1\) 情况的出现( \(DAG\) 图拓扑排序),且避免后代价一定更优.最坏也只有\(\frac{n(n+1)}{2}\ < \ n^2\).若在位置 \(0\) 加上一个虚拟根作为所有字符串的前缀,那么情况 \(3\) 可以看做是 \(2\) 的特殊情况,可以一起处理.

  • 将每个字符串作为一个节点,给节点编号,求节点编号与父亲编号差的最小值即可.

  • 贪心地做, \(dfs\) 按照子树大小从小到大选出来先编号即可.

View code

#include"bits/stdc++.h"
using namespace std;
typedef long long LoveLive;
inline int read()
{
    int out=0,fh=1;
    char jp=getchar();
    while ((jp>'9'||jp<'0')&&jp!='-')
        jp=getchar();
    if (jp=='-')
        {
            fh=-1;
            jp=getchar();
        }
    while (jp>='0'&&jp<='9')
        {
            out=out*10+jp-'0';
            jp=getchar();
        }
    return out*fh;
}
const int MAXL=510010;
const int Siz=26;
const int MAXN=1e5+10;
int tot=0;
int cnt=0,head[MAXN],to[MAXN<<1],nx[MAXN<<1];
inline void add(int u,int v)
{
    ++cnt;
    to[cnt]=v;
    nx[cnt]=head[u];
    head[u]=cnt;
}
struct Trie{
    int idx;
    int ch[MAXL][Siz];
    int val[MAXL];
    Trie()
        {
            idx=0;
            memset(ch,0,sizeof ch);
            memset(val,0,sizeof val);
        }
    void ins(char buf[],int n,int v)
        {
            int u=0;
            for(int i=n-1;i>=0;--i)
                {
                    int k=buf[i]-'a';
                    if(!ch[u][k])
                        ch[u][k]=++idx;
                    u=ch[u][k];
                }
            val[u]=v;
        }
    void build_graph(int u,int pre)
        {
            ++tot;
            if(val[u])
                add(pre,val[u]),pre=val[u];
            for(int i=0;i
   
   
    
     pii;
pii tmp[MAXN];
stack
    
    
     
      stk;
void solve(int u,int fa)
{
    int pos=0;
    vis[u]=++tot;
    ans+=vis[u]-vis[fa];
    for(int i=head[u];i;i=nx[i])
        {
            int v=to[i];
            tmp[++pos]=make_pair(-siz[v],v);
        }
    sort(tmp+1,tmp+1+pos);
    for(int i=1;i<=pos;++i)
        stk.push(tmp[i].second);
    while(pos--)
        {
            int v=stk.top();
            stk.pop();
            solve(v,u);
        }
}
int main()
{
    int n=read();
    for(int i=1;i<=n;++i)
        {
            scanf("%s",buf);
            T.ins(buf,strlen(buf),i);           
        }
    T.build_graph(0,0);
    dfs(0);
    solve(0,0);
    printf("%lld\n",ans);
    return 0;
}

    
    
   
   

The XOR-longest Path

  • 题意:给一颗边权树,求出树上最长的异或和路径.
  • 利用异或的优秀性质,可以处理出节点 \(1\) 到每个点的距离 \(dis\) ,那么 \(u\)\(v\) 之间的异或和距离直接就是 \(dis[u]\) ^ \(dis[v]\) .被重复计算的部分自身异或两次抵消了.
  • 那么将 \(dis\) 数组求出后,问题就变为在这个数组中找两个数,使得这对数异或值最大.
  • 使用 \(Trie\) 树的经典做法解决即可.
View code

#include"bits/stdc++.h"
using namespace std;
typedef long long LoveLive;
inline int read()
{
    int out=0,fh=1;
    char jp=getchar();
    while ((jp>'9'||jp<'0')&&jp!='-')
        jp=getchar();
    if (jp=='-')
        {
            fh=-1;
            jp=getchar();
        }
    while (jp>='0'&&jp<='9')
        {
            out=out*10+jp-'0';
            jp=getchar();
        }
    return out*fh;
}
const int MAXN=1e5+10;
int head[MAXN],nx[MAXN<<1],to[MAXN<<1],val[MAXN<<1],cnt=0;
inline void add(int u,int v,int w)
{
    ++cnt;
    nx[cnt]=head[u];
    to[cnt]=v;
    val[cnt]=w;
    head[u]=cnt;
}
int n;
int dis[MAXN];
struct Trie{
    int idx;
    int ch[MAXN*10][2];
    Trie()
        {
            memset(ch,0,sizeof ch);
            idx=0;
        }
    void ins(int x)
        {
            int u=0;
            for(int i=31;i>=0;--i)
                {
                    int k=(x>>i)&1;
                    if(!ch[u][k])
                        ch[u][k]=++idx;
                    u=ch[u][k];
                }
        }
    int maxxor(int x)
        {
            int u=0,res=0;
            for(int i=31;i>=0;--i)
                {
                    int k=(x>>i)&1;
                    if(ch[u][k^1])
                        {
                            res<<=1;
                            res+=1;
                            u=ch[u][k^1];
                        }
                    else
                        {
                            res<<=1;
                            u=ch[u][k];
                        }
                }
            return res;
        }
}T;
void dfs(int u,int fa)
{
    for(int i=head[u];i;i=nx[i])
        {
            int v=to[i];
            if(v==fa)
                continue;
            dis[v]=dis[u]^val[i];
            dfs(v,u);
        }
}
int main()
{
    n=read();
    for(int i=1;i
   
   

   
   

小结

  • 将数字按照二进制位插入 \(Trie\) 树形成的东西应该是叫 \(01\ Trie\) 树...这个东东可以用来解决异或有关问题,还可以做到可持久化来解决区间问题(形态不改变,类似于主席树).若维护出节点 \(siz\) 后,可以解决平衡树的一类问题.涉及范围比较广泛,但大多还是只在解决异或问题时使用,因为其他时候我们可以使用我们更熟悉的数据结构.
  • 可持久化的 \(Trie\) 树实现咕了以后来补.
  • \(Trie\) 树的可持久化一般也只用于解决数字问题.

猜你喜欢

转载自www.cnblogs.com/jklover/p/10216247.html