1. 程式人生 > >字典樹(Trie)的學習筆記

字典樹(Trie)的學習筆記

按照一本通往下學,學到吐血了。。。

例題1

字典樹模板題嗎。

先講講字典樹:

在這裡插入圖片描述

給出程式碼(太簡單了。。。)!

#include<cstdio>
#include<cstring>
using  namespace  std;
struct  trie
{
    int  a[10],v;
    //a代表0~9的兒子的編號,有則為這個節點的編號,沒有就是0。
    //v是附加權值,代表每個節點被經過的次數。
}tr[200000];int  trlen;//多少節點
char  st[11000][20];int  n;
void  add(char  ss[])
{
    int  len=strlen(ss+1),root=0;//root代表根節點
    for(int  i=1;i<=len;i++)
    {
        int  k=ss[i]-'0';//計算是哪個兒子。。。
        if(tr[root].a[k]==0)tr[root].a[k]=++trlen;//新增新節點
        root=tr[root].a[k];tr[root].v++;//給這個節點加一次走過的標記
    }
}
int  find(char  ss[])
{
    int  len=strlen(ss+1),root=0;//root代表根節點
    for(int  i=1;i<=len;i++)//找到這個字串最底下的節點編號
    {
        int  k=ss[i]-'0';
        root=tr[root].a[k];
    }
    return  tr[root].v-1;//被經過兩次代表這個字串是某個字串的字首。
}
int  main()
{
    int  T;scanf("%d",&T);
    while(T--)
    {
        memset(tr,0,sizeof(tr));trlen=0;//初始化
        scanf("%d",&n);
        for(int  i=1;i<=n;i++)//新增
        {
            scanf("%s",st[i]+1);
            add(st[i]);
        }
        bool  bk=false;
        for(int  i=1;i<=n;i++)
        {
            if(find(st[i]))//發現答案
            {
                bk=true;
                printf("NO\n");//輸出
                break;
            }
        }
        if(bk==false)printf("YES\n");//同樣是輸出。
    }
    return  0;
}

例題二

我們發現只要把每個數字轉成31位二進位制,然後,一個個插入字典樹,在插入之前,我們計算一下當前的數與哪個數的異或值最大,並記錄一下。
至於如何找,只需要,我們只需要每次走與當前二進位制位數不一樣的數字就行了,看程式碼還挺好理解的(find函式)

#include<cstdio>
#include<cstring>
using  namespace  std;
struct  node
{
    int  a[2],v;//v是附加權值,代表這個二進位制數是多少
}tr[6100000];int  trlen;
char  st[40];
int  n,ans;
void  lintoto(int  x)//將x轉成二進位制數
{
    memset(st,0,sizeof(st));
    int  len=0;
    while(x)
    {
        st[++len]=(x&1);//相當於x%2。
        x>>=1;//除於2
    }
}
void  add(char  ss[],int  id)
{
    int  len=31,root=0;
    for(int  i=len;i>=1;i--)//由於當時存二進位制是倒著存的,現在也要倒著搜
    {
        int  k=ss[i];
        if(!tr[root].a[k])tr[root].a[k]=++trlen;//新建節點
        root=tr[root].a[k];
    }
    tr[root].v=id;//新增附加權值
}
inline  int  mymax(int  x,int  y){return  x>y?x:y;}
inline  int  find(char  ss[],int  id)
{
    int  root=0,len=31;
    for(int  i=len;i>=1;i--)
    {
        int  k=ss[i];
        if(tr[root].a[k^1])root=tr[root].a[k^1];//走相反的地方
        else  root=tr[root].a[k];//不存在,走相同的地方。
    }
    return  (tr[root].v^id);
}
int  main()
{
    scanf("%d",&n);
    for(int  i=1;i<=n;i++)
    {
        int  x;scanf("%d",&x);
        lintoto(x);//轉成二進位制
        if(i!=1)ans=mymax(ans,find(st,x));//更新新的答案
        add(st,x);//新增
    }
    printf("%d\n",ans);
    return  0;
}

例題三

先證明異或符合交換律、結合律。

首先,異或的過程中:(1,0),(0,1),(0,0)都是消掉一個0,而(1,1)是消掉一個1。

我們可以發現,多次異或就是看所有數二進位制的每一位的1的個數,奇數就為1,偶數為0,而你調換順序的話,個數並沒有變,所以滿足交換律與結合律。

那麼這道題,我們設\(sst_{i}=a_{1}⨁a_{2}⨁a_{3}⨁a_{4}...⨁a_{i}\),那麼,求區間最大,我們可以發現:

\(a_{l}⨁a_{l+1}⨁...⨁a_{r}=(a_{1}⨁a_{2}⨁a_{3}⨁...⨁a_{r})⨁(a_{1}⨁a_{2}⨁a_{3}⨁...⨁a_{l-1})=sst_{r}⨁sst_{l-1}\)


也就是說,我們只要求出在1~r-1區間內能異或\(sst_{r}\)的最大值,也就是上面那道題,設\(ll_{i}=\) 在1~i區間內的一個區間最大異或值,\(ll_{i}=max(ll_{i-1},以i為結尾的最大異或區間)\)

再設一個\(rr_{i}=\) 在i~n區間內的一個區間最大異或值,\(rr_{i}=max(rr_{i+1},以i為開頭的最大異或區間)\)

求出最大的\(ll_{i-1}+rr_{i}\)

#include<cstdio>
#include<cstring>
using  namespace  std;
struct  trie
{
    int  a[2],v;
}tr[9100000];int  trlen;//字典樹
char  st[210];
int  sst[410000],n,ll[410000],rr[410000],ans;//陣列
void  intto(int  x)//像上次那樣。。。
{
    memset(st,0,sizeof(st));
    int  len=0;
    while(x)
    {
        st[++len]=(x&1);//x%2
        x>>=1;//x/=2
    }
}
void  add(char  ss[],int  id)//新增
{
    int  len=31,root=0;
    for(int  i=len;i>=1;i--)
    {
        int  k=ss[i];
        if(!tr[root].a[k])tr[root].a[k]=++trlen;
        root=tr[root].a[k];
    }
    tr[root].v=id;
}
int  find(char  ss[],int  id)//尋找
{
    int  len=31,root=0;
    for(int  i=len;i>=1;i--)
    {
        int  k=ss[i];
        if(tr[root].a[k^1])root=tr[root].a[k^1];
        else  root=tr[root].a[k];
    }
    return  (tr[root].v^id);//返回異或值
}
inline  int  getsum(int  x,int  y){return  sst[y]^sst[x-1];}//[x,y]區間的異或值
inline  int  mymax(int  x,int  y){return  x>y?x:y;}//最大值
int  main()
{
    scanf("%d",&n);
    intto(0);
    add(st,0);//將0新增
    for(int  i=1;i<=n;i++)
    {
        scanf("%d",&sst[i]);sst[i]^=sst[i-1];
        intto(sst[i]);//二進位制
        ll[i]=mymax(find(st,sst[i]),ll[i-1]);//更新
        add(st,sst[i]);//新增
    }
    memset(tr,0,sizeof(tr));trlen=0;
    intto(0);
    add(st,0);
    for(int  i=n;i>=1;i--)
    {
        intto(getsum(i,n));
        rr[i]=mymax(rr[i-1],find(st,getsum(i,n)));
        add(st,getsum(i,n));
    }//反著來一遍
    for(int  i=2;i<=n;i++)ans=mymax(ans,ll[i-1]+rr[i]);//統計答案
    printf("%d\n",ans);
    return  0;
}

練習一

跟例題一 一樣。。。只不過處理方式出了點問題。

#include<cstdio>
#include<cstring>
#include<cstdlib>
using  namespace  std;
struct  node
{
    int  a[10],v;
}tr[2100];int  trlen;//字典樹
char  st[20];
void  add(char  ss[])//打得不能再順手的新增。。。
{
    int  len=strlen(ss+1),root=0;
    for(int  i=1;i<=len;i++)
    {
        int  k=ss[i]-'0';
        if(!tr[root].a[k])tr[root].a[k]=++trlen;
        root=tr[root].a[k];
    }
    tr[root].v=1;
}
bool  find(char  ss[])//每天都在變。。。
{
    int  len=strlen(ss+1),root=0;
    for(int  i=1;i<=len;i++)
    {
        int  k=ss[i]-'0';
        if(!tr[root].a[k])return  false;//不存在節點?返回
        root=tr[root].a[k];
        if(tr[root].v==1)return  true;//存在,返回
    }
    return  false;
}
int  main()
{
    int  T=0;
    while(scanf("%s",st+1)!=EOF)
    {
        T++;
        memset(tr,0,sizeof(tr));trlen=0;//初始化
        bool  bk=false;
        while(1)
        {
            int  len=strlen(st+1);
            if(len==1  &&  st[1]=='9')break;//退出
            if(find(st))bk=true;//尋找
            add(st);//新增
            scanf("%s",st+1);//輸入
        }
        if(bk==true)printf("Set %d is not immediately decodable\n",T);
        else  printf("Set %d is immediately decodable\n",T);
    }
    return  0;
}

練習二

將字典建個字典樹,然後類似遞推思想瞎搞。。。

提示:建一個d陣列代表這一段在前i位能否被表達。

#include<cstdio>
#include<cstring>
using  namespace  std;
struct  node
{
    int  a[26],v;
}tr[51000];int  trlen;//字典樹
char  st[2100000];//字串
int  n,m;
char  d[2100000];//d陣列
void  add(char  ss[])
{
    int  len=strlen(st+1),root=0;
    for(int  i=1;i<=len;i++)
    {
        int  k=st[i]-'a';
        if(!tr[root].a[k])tr[root].a[k]=++trlen;
        root=tr[root].a[k];
    }
    tr[root].v=1;
}
bool  find(int  l,int  r)//匹配l~r區間
{
    int  root=0;
    for(int  i=l;i<=r;i++)
    {
        int  k=st[i]-'a';
        if(!tr[root].a[k])return  false;
        root=tr[root].a[k];
    }
    return  tr[root].v;
}
inline  int  mymax(int  x,int  y){return  x>y?x:y;}
int  jie;
int  main()
{
    scanf("%d%d",&n,&m);
    for(int  i=1;i<=n;i++)
    {
        scanf("%s",st+1);
        add(st);
        jie=mymax(jie,strlen(st+1));//最長的單詞
    }
    for(int  kkk=1;kkk<=m;kkk++)
    {
        memset(d,0,sizeof(d));d[0]=1;//初始化
        scanf("%s",st+1);
        int  len=strlen(st+1);
        for(int  i=1;i<=len;i++)
        {
            for(int  j=mymax(1,i-jie+1);j<=i;j++)
            {
                if(d[j-1]==1  &&  find(j,i))//如果前j-1位已經匹配成功,那麼判斷當前能否匹配成功
                {
                    d[i]=1;
                    break;
                }
            }
        }
        int  ans=0;
        for(int  i=len;i>=1;i--)//統計
        {
            if(d[i]==1)
            {
                ans=i;break;
            }
        }
        printf("%d\n",ans);
    }
    return  0;
}

練習三

建立資訊為字典樹,兩個附加權值,代表被經過的次數和作為結尾的次數。

那密碼進行統計。。。

怎麼統計自己想或看程式碼

#include<cstdio>
#include<cstring>
using  namespace  std;
struct  trie
{
    int  a[2],v1,v2;//兩個附加權值,v1代表被經過次數,v2代表被作為結尾的次數
}tr[610000];int  trlen;//字典樹
char  st[510000];//字串
int  n,m;
void  add(int  len)//新增
{
    int  root=0;
    for(int  i=1;i<=len;i++)
    {
        int  k=st[i];
        if(!tr[root].a[k])tr[root].a[k]=++trlen;
        root=tr[root].a[k];tr[root].v1++;//經過一次
    }
    tr[root].v2++;//作為結尾加一
}
int  find(int  len)
{
    int  root=0,ans=0;
    for(int  i=1;i<=len;i++)
    {
        int  k=st[i];
        if(!tr[root].a[k])return  ans;
        root=tr[root].a[k];ans+=tr[root].v2;//有多少是他的字首
    }
    ans+=tr[root].v1-tr[root].v2;//他是多少字串的字尾
    return  ans;
}
int  main()
{
    scanf("%d%d",&n,&m);
    for(int  i=1;i<=n;i++)
    {
        int  len;scanf("%d",&len);
        for(int  j=1;j<=len;j++)scanf("%d",&st[j]);//輸入字串
        add(len);//新增
    }
    for(int  i=1;i<=m;i++)
    {
        int  len;scanf("%d",&len);
        for(int  j=1;j<=len;j++)scanf("%d",&st[j]);//輸入字串
        printf("%d\n",find(len));//尋找
    }
    return  0;
}

練習四

這道題目QAQ。

首先,我們知道,長度為s1的字串a1,長度為s2的字串a2,如果\(s1<s2\)並且a1與a2都是a3的字尾,那麼a1是a2的字尾。

將所有字串翻轉,建一個字典樹,然後建一顆樹,當\(i\)->\(j\)僅當,\(a_{i}\)\(a_{j}\)的最大字首。

然後,DFS新樹,每次進入沒進過且節點最少的樹就行了。

(如果直接在字典樹上跑的話,子樹大小會錯亂,導致每次進的不是節點最小的樹)

這道題還是有點難的,因為用到了一點貪心思想。

#include<cstdio>
#include<cstring>
#include<algorithm>
using  namespace  std;
char  st[520000];
int  n;
bool  bol[110000];
struct  trie//字典樹
{
    int  a[26],v;
}tr1[610000];int  trlen1;
struct  node//新建的樹
{
    int  y,next;
}tr2[110000];int  trlen2,last[110000],size[110000];//用邊目錄儲存
void  ins(int  x,int  y)//新建一條邊
{
    trlen2++;
    tr2[trlen2].y=y;tr2[trlen2].next=last[x];last[x]=trlen2;
}
void  add(int  id)//新增
{
    int  root=0,len=strlen(st+1);
    for(int  i=len;i>=1;i--)//字串翻轉
    {
        int  k=st[i]-'a';
        if(!tr1[root].a[k])tr1[root].a[k]=++trlen1;
        root=tr1[root].a[k];
    }
    tr1[root].v=id;
}
int  dfss(int  x,int  fa)//遍歷字典樹,建新樹,fa是離他最近的有權的祖先
{
    int  ans=0;//代表子樹大小
    for(int  i=0;i<=25;i++)
    {
        if(tr1[x].a[i])
        {
            if(tr1[x].v)ans+=dfss(tr1[x].a[i],tr1[x].v);//本身是一個節點
            else  ans+=dfss(tr1[x].a[i],fa);//本身不是一個節點,只是一箇中轉站
        }
    }
    if(tr1[x].v)//本身是一個節點
    {
        ins(fa,tr1[x].v);//連向父親
        ans++;size[tr1[x].v]=ans;//繼承大小
    }
    return  ans;//返回目前的子樹大小
}
long  long  anss;
int  times,lis[110000],listlen;
bool  cmp(int  x,int  y){return  size[x]<size[y];}
void  dfs(int  x,int  nowss)
{
    int  now=++times;
    int  l=listlen+1,r;
    for(int  k=last[x];k;k=tr2[k].next)lis[++listlen]=tr2[k].y;//處理list
    r=listlen;
    if(l<=r)sort(lis+l,lis+1+r,cmp);//排序是排[l,r) 
    else  return  ;//沒有子樹 
    for(int  i=l;i<=r;i++)
    {
        anss+=(times+1)-now;//統計答案 
        dfs(lis[i],now);//繼續遞迴 
    }
}
int  main()
{
    scanf("%d",&n);
    for(int  i=1;i<=n;i++)
    {
        scanf("%s",st+1);
        add(i);
    }//新增所有字串
    dfss(0,0);//建樹 
    dfs(0,0);//處理答案 
    printf("%lld\n",anss);//輸出 
    return  0;
}

練習五

這道題,我一開始想得太複雜了。。。

我一開始想到的是一條路徑上,起點是l,終點是r,然後還涉及到求LCA,然後就十分複雜。。。

然後膜了一波題解,發現自己就是個弱智。。。

\(sum[i]=(1->i)\)的權值異或值 ,那麼一條路徑上的異或權值和就是\(sum[l]\)^\(sum[j]\),然後就是求一個數組中求與\(sum[i]\)異或值最大的數,其實就是例題了,根本不用在樹上進行特別麻煩的遍歷,或者是過程很麻煩的樹上遞迴,是不是很巧妙?好吧,我承認就是我沒有想到然後就亂膜。。。

#include<cstdio>
#include<cstring>
using  namespace  std;
struct  node
{
    int  y,next,c;
}a[210000];int  last[110000],alen;
int  sum[110000];//邊目錄 
void  ins(int  x,int  y,int  c)
{
    alen++;
    a[alen].y=y;a[alen].c=c;a[alen].next=last[x];last[x]=alen;
}
void  dfs(int  x,int  fa)
{
    for(int  k=last[x];k;k=a[k].next)
    {
        int  y=a[k].y;
        if(y!=fa)//不是他的父親 
        {
            sum[y]=sum[x]^a[k].c;//處理sum陣列 
            dfs(y,x);//繼續遞迴 
        }
    }
}
struct  trie
{
    int  a[2],v;
}tr[4100000];int  trlen;//字典樹 
char  st[40];//儲存二進位制的陣列。 
void  intoo(int  x)
{
    memset(st,0,sizeof(st));//初始化 
    int  len=0;
    while(x)
    {
        st[++len]=(x&1);//x%2
        x>>=1;//x/=2
    }
}
void  add(int  id)//新增 
{
    int  len=31,root=0;
    for(int  i=len;i>=1;i--)//新增二進位制要注意的 
    {
        int   k=st[i];
        if(!tr[root].a[k])tr[root].a[k]=++trlen;
        root=tr[root].a[k];
    }
    tr[root].v=id;
}
int  find(int  id)
{
    if(trlen==0)return  0;//判斷目前字典樹是否有節點 
    int  len=31,root=0;
    for(int  i=len;i>=1;i--)
    {
        int  k=st[i];
        if(tr[root].a[k^1])root=tr[root].a[k^1];//貪心找異或值最大 
        else  root=tr[root].a[k];
    }
    return  (id^tr[root].v);//給出結果 
}
int  n;
inline  int  mymax(int  x,int  y){return  x>y?x:y;}//找最大值 
int  main()
{
    scanf("%d",&n);
    for(int  i=1;i<n;i++)
    {
        int  x,y,c;scanf("%d%d%d",&x,&y,&c);
        ins(x,y,c);ins(y,x,c);
    }
    dfs(1,0);//預設1為根,處理sum陣列 
    int  ans=0;
    for(int  i=1;i<=n;i++)//例題的做法 
    {
        intoo(sum[i]);
        ans=mymax(ans,find(sum[i]));
        add(sum[i]);
    }
    printf("%d\n",ans);
    return  0;
}

終於寫完了。。。