字典樹(Trie)的學習筆記
按照一本通往下學,學到吐血了。。。
字典樹模板題嗎。
先講講字典樹:
給出程式碼(太簡單了。。。)!
#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;
}
終於寫完了。。。