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 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; }