Tags:字符串 題解html
廣義後綴自動機實際上考得比普通後綴自動機要更多更靈活
因此這裏做爲一個小專題呈現,題單在後綴自動機的總題單裏
爲了更好掌握廣義\(SAM\),這裏提供一個高級模板題的題解node
普通後綴自動機處理單串的問題,多串就只能使用廣義\(SAM\)了ios
最方便的構建學習
一種方案是把字符串中間依次用從未出現過的字符鏈接,看成一個字符串處理,再多幾個特判spa
for(int i=1;i<=m;i++) { string A; cin>>A; s[++l]='#'; for(int j=0,l=A.size();j<l;j++) s[++l]=A[j]; }
一種方案是每插入一個串就把\(lst=1\),其他什麼都不用改變3d
for(int i=1;i<=m;i++) { lst=1; cin>>s; l=s.size(); for(int j=0;j<l;j++) Extend(s[j]-'0'); }
最準確的構建code
其實準確來講,廣義後綴自動機是經過遍歷Trie樹建的,因此要先建好\(Trie\)樹
每次找到\(fa[x]\)的節點做爲\(lst\)日後接便可htm
有兩種方法:\(BFS\)與\(DFS\)遍歷建後綴自動機
葉大佬告訴咱們,\(DFS\)會被卡成\(n^2\)而\(BFS\)不會,就像下面圖片中的例子blog
int Extend(int f,int c) { if(ch[f][c]&&len[ch[f][c]]==len[f]+1) return ch[f][c];//?! int p=++node,ff=0;lst=p; len[p]=len[f]+1; while(f&&!ch[f][c]) ch[f][c]=p,f=fa[f]; if(!f) {fa[p]=1;return p;} int x=ch[f][c],y=++node; if(len[x]==len[f]+1) {fa[p]=x;node--;return p;} if(len[p]==len[f]+1) ff=1;//?! memcpy(ch[y],ch[x],sizeof(ch[y])); len[y]=len[f]+1; fa[y]=fa[x]; fa[x]=fa[p]=y; while(f&&ch[f][c]==x) ch[f][c]=y,f=fa[f];//緣由就在這 return ff?y:p; }
你會發現這和後綴自動機的模板有些區別誒
首先是有一個返回值,這個很好理解,返回的是若是下次要日後接的\(lst\),也就是方便找到\(Trie\)樹某點在\(SAM\)上的位置,直接傳進來就好啦
而後是有一個if(len[p]==len[f]+1) return y;
的特判
前面也有一個if(ch[f][c]&&len[ch[f][c]]==len[f]+1) return ch[f][c];
的特判圖片
好,廣義\(SAM\)的構建就講完了
什麼?!WTF?!那個特判是啥意思我還不造嘞!
別急,咱們來經過例題理解它
連接:HN省隊集訓
題意:給\(n\)個字符串\(s\),強制在線進行\(4\)個操做,詢問/串長\(1e5\),\(n\le20\)
題解
對於操做\(1\)
記錄\(pos[i][j]\)表示在第\(i\)次操做後\(j\)號串的結尾字符在\(SAM\)上的節點編號是\(j\),當在第\(i\)次操做在字符串\(x\)後插入字符時,令\(lst=pos[i-1][x]\),而後照常插入便可
對於操做\(2\)
記錄\(siz[i][j]\)表示在\(i\)號點,貢獻給\(j\)號字符串的\(Endpos\)集合的大小,實際意義就是\(i\)號點表明的字符串集合在\(n\)個串中共出現了\(\sum_{j=1}^{n}siz[i][j]\),其中在\(j\)號串中出現了\(siz[i][j]\)次。
查詢\(z\)串中\(x\)串的出現次數,那麼\(x\)串在\(y\)次操做後的結尾位置是\(pos[y][x]\),那麼\(siz[pos[y][x]][z]\)就是答案。
這裏的\(siz\)是在\(parent\)樹上對子樹進行求和纔有上述意義,原理可見於個人另外一篇博文後綴自動機,可是這裏須要在線插入節點,因而咱們用\(lct\)維護\(parent\)樹上的\(siz\),每次斷開或者連上父親的時候至關是給節點到\(parent\)根的路徑\(+/-1\),因此這裏的\(lct\)只須要支持鏈加,不須要改變樹的根因此沒有\(makeroot\)、\(reverse\)等操做,若是須要學習\(lct\),可見個人另外一篇博文lct
對於操做\(3\)
維護一個全局變量\(sum\),每一個節點貢獻的本質不一樣的子串個數就是\(len[x]-len[fa[x]]\)
對於操做\(4\)
在\(SAM\)上匹配到T的結束位置的節點,\(Ans=max(siz[pos][i]),i=1..n\)
複雜度分析
操做\(1\)的總複雜度是\(\sum len[s_i]×log(n)×20\)的,操做\(2\)是\(log(n)\)的,操做\(3\)是\(O(1)\),操做\(4\)是\(\sum len[T_i]+20\)的,\(log\)都是\(lct\)的複雜度
因此總共時間複雜度是\(O(\sum len[s_i]*log(n)+\sum len[T_i])\)的
空間複雜度\(O(\sum|S|*logn*20)\)左右
代碼以下
#include<iostream> #include<cstdio> #include<cstdlib> #include<cstring> #define ll long long using namespace std; int read() { char ch=getchar();int h=0,t=1; while((ch>'9'||ch<'0')&&ch!='-') ch=getchar(); if(ch=='-') t=-1,ch=getchar(); while(ch>='0'&&ch<='9') h=h*10+ch-'0',ch=getchar(); return h*t; } const int N=1e6+100; char s[N]; int n,zx,m,ans,node=1; int fa[N],ch[N][11],len[N],pos[N][21]; ll sum; namespace lct { #define lc t[x].ch[0] #define rc t[x].ch[1] struct LCT{int fa,ch[2],val[21],tag[21];}t[N]; int isrt(int x) {return t[t[x].fa].ch[0]!=x&&t[t[x].fa].ch[1]!=x;} void Tag(int x,int y,int k) {t[x].val[y]+=k; t[x].tag[y]+=k;} void pushdown(int x) { for(int i=1;i<=n;i++) { if(!t[x].tag[i]) continue; if(lc) Tag(lc,i,t[x].tag[i]); if(rc) Tag(rc,i,t[x].tag[i]); t[x].tag[i]=0; } } void rotate(int x) { int y=t[x].fa,z=t[y].fa; int k=t[y].ch[1]==x; if(!isrt(y)) t[z].ch[t[z].ch[1]==y]=x; t[x].fa=z; t[y].ch[k]=t[x].ch[k^1]; t[t[x].ch[k^1]].fa=y; t[x].ch[k^1]=y; t[y].fa=x; } void Push(int x){if(!isrt(x)) Push(t[x].fa);pushdown(x);} void splay(int x) { Push(x); while(!isrt(x)) { int y=t[x].fa,z=t[y].fa; if(!isrt(y)) (t[z].ch[0]==y)^(t[y].ch[0]==x)?rotate(x):rotate(y); rotate(x); } } void Access(int x) {for(int y=0;x;y=x,x=t[x].fa) splay(x),rc=y;} void Add(int x,int y,int op) {for(int i=1;i<=n;i++) Tag(x,i,t[y].val[i]*op);} void link(int x,int y) {Push(x);t[x].fa=y;Access(y);splay(y);Add(y,x,1);} void cut(int x) {Access(x);splay(x);Add(lc,x,-1);lc=t[lc].fa=0;} int query(int x,int k) {Push(x);return t[x].val[k];} } int Extend(int f,int id,int c) { if(ch[f][c]&&len[ch[f][c]]==len[f]+1) { int p=ch[f][c]; lct::Access(p);lct::splay(p);lct::Tag(p,id,1); return p; } int p=++node; len[p]=len[f]+1; lct::t[p].val[id]=1; while(f&&!ch[f][c]) ch[f][c]=p,f=fa[f]; if(!f) {fa[p]=1;lct::link(p,1);sum+=len[p]-len[fa[p]];return p;} int x=ch[f][c]; if(len[f]+1==len[x]) {fa[p]=x;lct::link(p,x);sum+=len[p]-len[fa[p]];return p;} if(len[f]+1==len[p])//!! { lct::cut(x);lct::link(p,fa[x]);lct::link(x,p); memcpy(ch[p],ch[x],sizeof(ch[p])); fa[p]=fa[x]; fa[x]=p; sum-=len[p]-len[fa[p]]; while(f&&ch[f][c]==x) ch[f][c]=p,f=fa[f]; } else { int y=++node; len[y]=len[f]+1; lct::cut(x);lct::link(y,fa[x]);lct::link(x,y);lct::link(p,y); memcpy(ch[y],ch[x],sizeof(ch[y])); fa[y]=fa[x]; fa[x]=fa[p]=y; while(f&&ch[f][c]==x) ch[f][c]=y,f=fa[f]; } sum+=len[p]-len[fa[p]]; return p; } int calc() { int x=1,res=0,i,l; scanf("%s",s+1); for(i=1,l=strlen(s+1);i<=l&&x;i++) x=ch[x][s[i]-'0']; if(i!=l+1) return 0; lct::Push(x); for(i=1;i<=n;i++) res=max(res,lct::t[x].val[i]); return res; } int main() { n=read();zx=read(); for(int i=1;i<=n;i++) { scanf("%s",s+1); pos[0][i]=1; for(int p=1,l=strlen(s+1);p<=l;p++) pos[0][i]=Extend(pos[0][i],i,s[p]-'0'); } m=read(); for(int i=1;i<=m;i++) { int op=read(),x,y,z; for(int p=1;p<=n;p++) pos[i][p]=pos[i-1][p]; if(op<=2) x=read(),y=read(); if(op==1) y=(y^(ans*zx))%10,pos[i][x]=Extend(pos[i][x],x,y); else if(op==2) z=read(),printf("%d\n",ans=lct::query(pos[y][x],z)); else if(op==3) printf("%lld\n",sum); else printf("%d\n",ans=calc()); } return 0; }
Wait!
出題人和我講這道題的時候,特別強調要特判!
可是我一直不理解,把特判if(len[f]+1==len[p]) ff=1;
刪了後仍是過了本題
因而他給了一組\(hackdata\)而且在\(BZOJ\)增強了數據
Input 2 0 3201 01 1 2 2 0 1 Output Right : 1 Wrong : 0
首先咱們建出第一個串的\(SAM\)(黑邊是\(SAM\)上的邊,基佬紫邊是\(Parent\)樹上的邊)
咱們先加特判,那麼繼續建就會這樣(姨媽紅邊是刪去的邊)
繼續加邊
可是若是不加這個特判,就會撒夫夫地把藍\(4\)給複製一邊,可是這時第二個串的\(0\)的結尾位置是\(5\),當我查詢\(2\)串在\(1\)串中出現次數就會訪問到\(siz[pos[x][2]][1]\),此時很遺憾地發現調用到了\(5\)節點,它沒有記錄藍\(4\)的信息
這樣寫代碼很長,有沒有一種簡單的寫法捏?
有的,咱們發現黑\(5\)不會再被訪問到,至關於他的貢獻被算到了六號點上,同時又沒有點指向它因此它不會被訪問到。對比上面兩張圖,咱們發現其實就是\(圖1\)中的黑\(5\)和\(圖3\)中的黑\(6\)是徹底同樣的誒!
因而直接返回黑\(6\)就好啦
把上述代碼的\(Extend\)改爲下面這種
int Extend(int f,int id,int c) { if(ch[f][c]&&len[ch[f][c]]==len[f]+1) { int p=ch[f][c]; lct::Access(p);lct::splay(p);lct::Tag(p,id,1); return p; } int p=++node,ff=0; len[p]=len[f]+1; lct::t[p].val[id]=1; while(f&&!ch[f][c]) ch[f][c]=p,f=fa[f]; if(!f) {fa[p]=1;lct::link(p,1);sum+=len[p]-len[fa[p]];return p;} int x=ch[f][c]; if(len[f]+1==len[x]) {fa[p]=x;lct::link(p,x);sum+=len[p]-len[fa[p]];return p;} if(len[f]+1==len[p]) ff=1; int y=++node; len[y]=len[f]+1; lct::cut(x);lct::link(y,fa[x]);lct::link(x,y);lct::link(p,y); memcpy(ch[y],ch[x],sizeof(ch[y])); fa[y]=fa[x]; fa[x]=fa[p]=y; while(f&&ch[f][c]==x) ch[f][c]=y,f=fa[f]; sum+=len[p]-len[fa[p]]; return ff?y:p; }
是否是很方便?
if(ch[f][c]&&len[ch[f][c]]==len[f]+1) return ch[f][c];
咱們建\(SAM\)是在\(Trie\)樹上遍歷建的,這句話表示訪問到\(Trie\)樹上一個點,它在\(SAM\)上已經被建出來了,因此不須要新建
if(len[f]+1==len[p]) ff=1;
這句特判起做用的條件是中間\(f\)沒有被改變,也就是說存在\(ch[f][c]\),假設\(f\)表示的字符串是\(A\),那麼插入字符\(c\)後產生\(Ac\),可是\(Ac\)已經存在於自動機上,因此要把它從原來的狀態剝離出來,複製一遍,從後來的操做中能夠發現最後咱們須要的\(lst\)也就是\(return\)的點應該是複製出來的那個點