HihoCoder 字尾自動機入門1-6題解
比起字尾陣列,我覺得字尾自動機比較好理解也。。。
#1441 : 字尾自動機一·基本概念
endpos集合相同的子串才是一個同一個狀態。暴力模擬即可。
#include<bits/stdc++.h> #include<tr1/unordered_map> using namespace std; typedef long long ll; tr1::unordered_map<string,ll> mmp; tr1::unordered_map<ll,string> shortest,longest; int main(){String模擬string s,ss; cin>>s; int n,lens=s.size(); ll state; for(int i=0;i<lens;i++) for(int j=1;j<=lens-i;j++){ state=0; ss=s.substr(i,j); for(int k=0;k<=lens-j;k++){ if(ss==s.substr(k,j)) state|=(1ll<<(k+j-1)); }if(!shortest.count(state)||shortest[state].size()>j) shortest[state]=ss; if(!longest.count(state)||longest[state].size()<j) longest[state]=ss; mmp[ss]=state; } cin>>n; while(n--){ cin>>ss; state=mmp[ss]; cout<<shortest[state]<<""<<longest[state]; for(int i=0;i<lens;i++){ if(state&(1ll<<i)) printf(" %d",i+1); } printf("\n"); } return 0; }
#1445 : 字尾自動機二·重複旋律5
求不同子串的個數,而每個狀態中len[i]為這個狀態的最長子串長度,而它最短子串長度為len[link[i]]+1(因為它的字尾是在link[i]處斷的嘛)
所以每個狀態最長子串長度減去最短子串長度+1就是這個狀態有多少種不同子串,然後全部加起來即可。
#include<bits/stdc++.h> using namespace std; typedef long long ll; const int N=2e6+11; char s[N]; int size,last,maxlen[N];//minlen[N]; //擁有相同endpos集合的為同一狀態 //對於同一狀態中的字串,他們都是該狀態最長子串的字尾 //size總狀態數,last上一個狀態編號,maxlen[i]:i狀態包含的最長子串長度 int link[N],trans[N][31]; //trans[i][j] 轉移函式,為i狀態遇到j字元會轉移到哪個狀態 //link[i] SuffixLinks,i狀態的連續後綴在哪個狀態斷開 void initsam(int n){ size=last=1; for(int i=0;i<=n;i++){ link[i]=maxlen[i]=0;//minlen[i]=0; for(int j=0;j<26;j++) trans[i][j]=0; } } void extend(int x){ int cur=++size,u; maxlen[cur]=maxlen[last]+1; //Suffixpath(cur-S)路徑上沒有對x的轉移的狀態,新增到cur的轉移 for(u=last;u&&!trans[u][x];u=link[u]) trans[u][x]=cur; //若Suffixpath(cur-S)路徑上的狀態都沒有對x的轉移,那麼此時curlink到初狀態即可 if(!u) link[cur]=1; else{ //若Suffixpath(cur-S)路徑存在有對x轉移的狀態u //而v是u遇到x後轉移到的狀態 int v=trans[u][x]; //若v中最長的子串新增上x便是u的最長子串,此時將curlink到v //也就是v狀態中的子串都是cur狀態中的字尾,且cur的字尾序列剛好在v處斷開 if(maxlen[v]==maxlen[u]+1) link[cur]=v; else{ //否則建立箇中間狀態進行轉移 //也就是cur狀態和v狀態都有著部分相同的字尾,而之前這些字尾儲存在v狀態 //而v狀態中還有些狀態不是cur狀態的字尾的,所以需要個新狀態表示他們共有的字尾 int clone=++size; maxlen[clone]=maxlen[u]+1; memcpy(trans[clone],trans[v],sizeof(trans[v])); link[clone]=link[v]; // minlen[clone]=maxlen[link[clone]]+1; //原先新增x後轉移到v的狀態,現在都轉移到中間狀態 for(;u&&trans[u][x]==v;u=link[u]) trans[u][x]=clone; //最後,因為cur狀態和v狀態的字尾都在中間狀態這裡斷開 //所以cur和v都link到中間狀態 link[cur]=link[v]=clone; // minlen[v]=maxlen[link[v]]+1; } } // minlen[cur]=maxlen[link[cur]]+1; last=cur; return ; } int main(){ scanf("%s",s); int lens=strlen(s); initsam(2*lens); for(int i=0;i<lens;i++) extend(s[i]-'a'); ll ans=0; for(int i=2;i<=size;i++) ans+=maxlen[i]-maxlen[link[i]]; printf("%lld\n",ans); return 0; }入門往往那麼容易
#1449 : 字尾自動機三·重複旋律6
要求每個長度的出現個數,endpos集合就是每個狀態裡子串出現的個數,那麼麼endpos怎麼求呢,直接搬運HihoCoder的講解了,感覺講得很好,侵刪。。。。。。
小Ho:我們明白了。一個狀態st對應的|endpos(st)|至少是它兒子的endpos大小之和。這一點還是比較容易證明的。假設x和y是st的兩個兒子,那麼根據Suffix Link的定義,我們知道st中的子串都是x中子串的字尾,也是y中子串的字尾。所以endpos(st) ⊇ endpos(x) 並且 endpos(st) ⊇ endpos(y)。又根據Suffix Link的定義我們知道x中的子串肯定不是y中子串的字尾,反之亦然,所以endpos(x) ∩ endpos(y) = ∅。所以|endpos(st)| >= |endpos(x)| + |endpos(y)|。
小Hi:那麼|endpos(st)|可能比st兒子的endpos大小之和大多少呢?
小Ho:最多就大1。並且大1的情況當且僅當st是上文提到的綠色狀態,即st包含S的某個字首時才發生。我們分析endpos(1)={1, 2, 5}就會發現,它比endpos(2) ∪ endpos(6) = {2, 5}多出來的結束位置1的原因就是狀態1還包含S的長度為1的字首"a"。更一般的情形是如果某個狀態st包含S的一個字首S[1..l],那麼一定有l∈endpos(st),並且l不能從st的兒子中繼承過來。這時就需要+1。
小Hi:沒錯。那麼我們如何判斷哪些狀態應該標記成綠色狀態呢?
小Ho:可以在構造SAM的時候順手做了。回顧我們構造SAM的演算法,當新加入一個字元的時候,我們至少會新建一個狀態z(還可能新建一個狀態y),這個狀態z一定是綠色狀態(y一定不是)。
小Hi:沒錯,我們回顧一下。先構造SAM,順手把綠色狀態標記出來。然後再對Suffix Link連成的樹"自底向上"求出每一個狀態的|endpos(st)|,這一步"自底向上"可以通過拓撲排序完成,我們很早之前就講過,不再贅述。
所以就是每個新增字元的那個狀態endpos大小是1,然後再對SuffixLink樹進行拓撲排序,就可以得到每個狀態的endpos大小了。(dfs回溯也可以)
知道每個狀態endpos之後,我們又知道每個狀態的最長子串長度,它的影響範圍就是小於等於它的長度,所以記錄下相應長度的endpos再從後往前求最大值即可。
#include<bits/stdc++.h> using namespace std; const int N=2e6+11; struct Side{ int v,ne; }S[N]; char s[N]; int sn,head[N]; int size,last,len[N],link[N],trans[N][31]; int endpos[N],ans[N]; void initS(int n){ sn=0; for(int i=0;i<=n;i++) head[i]=-1; } void addS(int u,int v){ S[sn].v=v; S[sn].ne=head[u]; head[u]=sn++; } void initsam(int n){ size=last=1; for(int i=0;i<n;i++){ len[i]=link[i]=endpos[i]=0; for(int j=0;j<26;j++) trans[i][j]=0; } } void extend(int x){ int cur=++size,u; endpos[cur]=1; len[cur]=len[last]+1; for(u=last;u&&!trans[u][x];u=link[u]) trans[u][x]=cur; if(!u) link[cur]=1; else{ int v=trans[u][x]; if(len[v]==len[u]+1) link[cur]=v; else{ int clone=++size; len[clone]=len[u]+1; link[clone]=link[v]; memcpy(trans[clone],trans[v],sizeof(trans[v])); for(;u&&trans[u][x]==v;u=link[u]) trans[u][x]=clone; link[cur]=link[v]=clone; } } last=cur; } void dfs(int u){ for(int i=head[u];~i;i=S[i].ne){ int v=S[i].v; dfs(v); endpos[u]+=endpos[v]; } } void solve(int lens){ initS(size); for(int i=1;i<=size;i++) addS(link[i],i); dfs(1); for(int i=2;i<=size;i++) ans[len[i]]=max(ans[len[i]],endpos[i]); for(int i=lens-1;i>=1;i--) ans[i]=max(ans[i],ans[i+1]); } int main(){ scanf("%s",s); int lens=strlen(s); initsam(2*lens); for(int i=0;i<lens;i++) extend(s[i]-'a'); solve(lens); for(int i=1;i<=lens;i++) printf("%d\n",ans[i]); return 0; }進門了卻出不去
#1457 : 字尾自動機四·重複旋律7
先不管兩個串,就單有一個串的時候,我們怎麼算它的不同子串權值和呢,這就涉及動態規劃了。
比如我們知道子串12的權值為12,那麼怎麼得到子串123的權值呢,很簡單,12*10+3嘛。而有些狀態不一定只包含一個子串,但它們加上一個新字元x後的轉移狀態是相同的。
也就是說,如果我們知道了某個狀態的所有子串權值和sum(u),而trans[u][x]=v(u中的所有子串加上x後就變成v中的部分子串),那麼sum(v)+=sum(u)*10+x*u中所有子串的個數。
知道這個轉移過程之後,我們就可以根據trans確定拓撲順序,然後在上面進行轉移。
那如果是兩個串,我們也可以像前面字尾陣列一樣,用一個不會出現的字元間隔開,然後把它們連線起來。這裡使用:,因為:的ascii碼值為9的ascii值+1,好處理。
然後有些狀態中就會含有一些含:的子串,而這些子串是不合法的,所以我們轉移的時候跳過這些不合法的子串即可,怎麼跳過呢,就是不對trans[u][:]進行處理。
那麼新的轉移過程就是sum(v)+=sum(u)*10+x*u中所有合法子串的個數。
#include<bits/stdc++.h> using namespace std; typedef long long ll; const int N=2e6+11,md=1e9+7; char s[N]; ll sum[N]; queue<int> q; int du[N],fcnt[N]; int size,last,len[N],link[N],trans[N][21]; void initsam(int n){ size=last=1; for(int i=0;i<=n;i++){ len[i]=link[i]=0; for(int j=0;j<11;j++) trans[i][j]=0; } } void extend(int x){ int cur=++size,u; len[cur]=len[last]+1; for(u=last;u&&!trans[u][x];u=link[u]) trans[u][x]=cur; if(!u) link[cur]=1; else{ int v=trans[u][x]; if(len[v]==len[u]+1) link[cur]=v; else{ int clone=++size; len[clone]=len[u]+1; link[clone]=link[v]; memcpy(trans[clone],trans[v],sizeof(trans[v])); for(;u&&trans[u][x]==v;u=link[u]) trans[u][x]=clone; link[cur]=link[v]=clone; } } last=cur; return ; } void solve(){ for(int i=1;i<=size;i++){ sum[i]=fcnt[i]=0; for(int j=0;j<11;j++){ if(trans[i][j]) du[trans[i][j]]++; } } fcnt[1]=1; q.push(1); while(!q.empty()){ int u=q.front(); q.pop(); for(int i=0;i<11;i++){ int v=trans[u][i]; if(!v) continue; if(i!=10){ fcnt[v]+=fcnt[u]; sum[v]=(sum[v]+(sum[u]*10%md+i*fcnt[u])%md)%md; }//不轉移含:的子串 du[v]--; if(!du[v]) q.push(v); } } } int main(){ int n; initsam(N-1); scanf("%d",&n); for(int i=0;i<n;i++){ scanf("%s",s); int lens=strlen(s); for(int j=0;j<lens;j++) extend(s[j]-'0'); if(i!=n-1) extend(10);//類似字尾陣列中用#分隔兩個串 } solve(); ll ans=0; for(int i=1;i<=size;i++) ans=(ans+sum[i])%md; printf("%lld\n",ans); return 0; }如果要說個建議
#1465 : 字尾自動機五·重複旋律8
如果串不迴圈旋轉的話,那就是T串在S串中出現的次數,也就是看T串在S串的SAM中是哪個狀態u,那麼endpos[u]就是答案了。
而找T串在S串的SAM中的狀態的過程,其實也類似於找T串和S串的LCS(最長公共子串),如果到達某個狀態的LCS是T串的長度,這時就找到了。
怎麼用SAM找S串和T串的LCS呢,我們對S串建SAM,那麼接下來用T串在S串上面匹配。一開始u等於初始狀態,而lcs=0。
對於T[i],如果trans[u][T[i]]不為空的話,很明顯lcs++,然後u=trans[u][T[i]]。而當trans[u][T[i]]為空怎麼辦 ,我們就可以根據link[u],suffix-path(u->S)向前找trans[u][T[i]]不為空的狀態。
而這個過程就類似於KMP中失配時,按next陣列往回找的過程。若一直到最初狀態,rans[u][T[i]]依舊為空,那麼說明S串中無T[i]字元,讓u為最初狀態,lcs為0。
while(u!=1&&!trans[u][x]) u=link[u],lcs=len[u];//往回找trans[u][x]不為空的狀態 if(trans[u][x]) lcs++,u=trans[u][x]; else u=1,lcs=0;
而這個的T串還會進行迴圈,對於迴圈串的一種解決辦法就是把它拆成一條鏈,把原來的串拷貝一份放到後面。T[i]'=T'[n+i]=T[i]
然後遍歷T'[i],求出在每個位置T'[i]結束的最長公共子串,可以知道u和lcs。如果這時lcs>=T串的長度n,那我們就得到了一個公共子串T'[i-lcs+1 .. i]。
這個子串在S中出現的次數是|endpos(u)|,又恰好包含T的迴圈同構串T'[i-n+1 .. i]。而像aaa串,它某些迴圈串是相同的,這時就每個狀態u只統計一次即可。
但還有一種情況,要區分T'[i-lcs+1 .. i]出現次數和T'[i-n+1 .. i]的出現次數。lcs>=n,T'[i-n+1 .. i]是T'[i-lcs+1 .. i]不一定在同一個狀態u。
T'[i-n+1 .. i]是T'[i-lcs+1 .. i]長度為n的字尾,可能在suffix-path(u->S)上,出現次數比T'[i-lcs+1 .. i]多(HihoCoder中這裡應該是打錯了)。
這時也好處理,我們順著suffix-path(u->S)往回找,找到最靠近S且最長子串長度仍然大於等於n的即可。
#include<bits/stdc++.h> using namespace std; const int N=2e5+11; struct Side{ int v,ne; }S[N]; char s[N],ss[N]; int sn,head[N]; int size,last,len[N],link[N],trans[N][31]; int endpos[N],vis[N],tu[N]; void initS(int n){ sn=0; for(int i=0;i<=n;i++) head[i]=-1; } void addS(int u,int v){ S[sn].v=v; S[sn].ne=head[u]; head[u]=sn++; } void initsam(int n){ size=last=1; for(int i=0;i<n;i++){ len[i]=link[i]=endpos[i]=0; for(int j=0;j<26;j++) trans[i][j]=0; } } void extend(int x){ int cur=++size,u; endpos[cur]=1; len[cur]=len[last]+1; for(u=last;u&&!trans[u][x];u=link[u]) trans[u][x]=cur; if(!u) link[cur]=1; else{ int v=trans[u][x]; if(len[v]==len[u]+1) link[cur]=v; else{ int clone=++size; len[clone]=len[u]+1; link[clone]=link[v]; memcpy(trans[clone],trans[v],sizeof(trans[v])); for(;u&&trans[u][x]==v;u=link[u]) trans[u][x]=clone; link[cur]=link[v]=clone; } } last=cur; } void dfs(int u){ for(int i=head[u];~i;i=S[i].ne){ int v=S[i].v; dfs(v); endpos[u]+=endpos[v]; } } void tp(){ initS(size); for(int i=1;i<=size;i++) addS(link[i],i); dfs(1); } int main(){ scanf("%s",s); int n,lens=strlen(s); initsam(2*lens); for(int i=0;i<lens;i++) extend(s[i]-'a'); tp(); scanf("%d",&n); while(n--){ scanf("%s",ss); int lenss=strlen(ss),u=1,lcs=0,ans=0,cnt=0; for(int i=0;i<lenss-1;i++) ss[lenss+i]=ss[i]; for(int i=0;i<2*lenss-1;i++){ int x=ss[i]-'a'; while(u!=1&&!trans[u][x]) u=link[u],lcs=len[u]; if(trans[u][x]) lcs++,u=trans[u][x]; else u=1,lcs=0; //處理T[i-lcs+1]跟T[i-n+1]不同狀態的情況 if(lcs>lenss){ while(len[link[u]]>=lenss) u=link[u]; lcs=len[u]; } //每個狀態只統計一次 if(lcs>=lenss&&!vis[u]){ vis[u]=1; tu[cnt++]=u; ans+=endpos[u]; } } for(int i=0;i<cnt;i++) vis[tu[i]]=0; printf("%d\n",ans); } return 0; }那就是不要進去
#1466 : 字尾自動機六·重複旋律9
不知道怎麼解釋,看程式碼吧,等語言表達能力提升,再來更新。
#include<bits/stdc++.h> using namespace std; typedef long long ll; const int N=2e5+11; struct Sam{ int size,last,len[N],link[N],trans[N][31],sg[N]; ll cnt[N][31]; //cnt[i][j]為以該狀態為字首,sg函式為j的子串個數 Sam(){ size=last=1; sg[1]=-1; } void extend(int x){ int cur=++size,u; sg[cur]=-1; len[cur]=len[last]+1; for(u=last;u&&!trans[u][x]; u=link[u]) trans[u][x]=cur; if(!u) link[cur]=1; else{ int v=trans[u][x]; if(len[v]==len[u]+1) link[cur]=v; else{ int clone=++size; sg[clone]=-1; len[clone]=len[u]+1; link[clone]=link[v]; memcpy(trans[clone],trans[v],sizeof(trans[v])); for(;u&&trans[u][x]==v;u=link[u]) trans[u][x]=clone; link[cur]=link[v]=clone; } } last=cur; } int Sg(int u){ if(sg[u]!=-1) return sg[u]; int vis[31]; for(int i=0;i<30;i++) vis[i]=0; for(int i=0;i<26;i++){ int v=trans[u][i]; if(v){ vis[Sg(v)]=1; for(int j=0;j<30;j++) cnt[u][j]+=cnt[v][j]; } } for(int i=0;i<30;i++){ if(!vis[i]){ sg[u]=i; cnt[u][i]++; break; } } for(int i=0;i<30;i++) cnt[u][30]+=cnt[u][i]; return sg[u]; } }A,B; ll k; char a[N],b[N],ansa[N],ansb[N]; int solvea(int u,int p){ //因為先找A串,B串此時為空串,這裡就是看 //B串sg不為A當前構造的這個串的sg的串有多少個 ll sum=B.cnt[1][30]-B.cnt[1][A.sg[u]]; //如果sum大於等於k,說明接下來再去構造B串即可 //此時A串就是字典序最小的 if(sum>=k){ ansa[p]='\0'; return u; } k-=sum; for(int i=0;i<26;i++){ int v=A.trans[u][i]; if(v){ sum=0; //這裡就是算當A串的p位為'a'+i時,B串可能的串有多少種 for(int j=0;j<30;j++){ sum+=A.cnt[v][j]*(B.cnt[1][30]-B.cnt[1][j]); } //如果sum小於k,說明A串的p位為'a'+i的話,不能達到k //還得往下一個字元找 if(sum<k) k-=sum; else{ //否則,A串的p位為'a'+i,繼續去找p+1為 ansa[p]='a'+i; return solvea(v,p+1); } } } return 0; } void solveb(int u,int p,int x){ k-=(B.sg[u]!=x); if(!k){ ansb[p]='\0'; return ; } for(int i=0;i<26;i++){ int v=B.trans[u][i]; //這裡就是看,B串的p位為'a'+i接下來能有多少能可能的串 ll sum=B.cnt[v][30]-B.cnt[v][x]; //同A串 if(sum<k) k-=sum; else{ ansb[p]='a'+i; solveb(v,p+1,x); return ; } } } int main(){ scanf("%lld%s%s",&k,a,b); int lena=strlen(a),lenb=strlen(b); for(int i=0;i<lena;i++) A.extend(a[i]-'a'); for(int i=0;i<lenb;i++) B.extend(b[i]-'a'); //預處理出兩個字串的每個狀態的sg和cnt A.Sg(1);B.Sg(1); int u=solvea(1,0); if(!u) printf("NO\n"); else{ solveb(1,0,A.sg[u]); printf("%s\n%s\n",ansa,ansb); } return 0; }兩行淚