北京培训记day2
后缀三姐妹
P.S.后缀大家族关系:后缀自动机fail指针=后缀树,后缀树前序遍历=后缀数组
一、后缀数组:orz罗穗骞集训队论文
给每个后缀按字典序排序
rank[]表示从i开始的后缀排名多少
sa[]表示排名为i的后缀是从哪儿开始的
倍增 & dc3(反正dc3我是不会QAQ)
每次倍增后将二元组桶排(我自己yy了一个vector的可能比较慢QAQ)
h数组按照原串顺序求,利用求上一位h后留下的信息
应用:RMQ,多串拼接,分组,穷举+判断......
//by xxb #include<iostream> #include<algorithm> #include<cstdio> #include<cstring> #include<vector> using namespace std; const int Mx=100010; int n,m,rank[Mx],mn[Mx][50],h[Mx],sa[Mx],rank1[Mx],tmp[Mx],temp=1,Ton[26],ton1[Mx],num[Mx]; char ch[Mx]; vector <int> v[Mx],ton[Mx]; void pre() { int temp1=1;num[1]=1;for(int i=1;i<=n;i++) { if(i==temp1*2) temp1*=2;num[i]=temp1; } for(int i=1;i<=n;i++) Ton[ch[i]-'a']=1; for(int i=0,lst=0;i<26;i++) if(Ton[i]==1) ton1[i]=ton1[lst]+1,lst=i; for(int i=1;i<=n;i++) rank[i]=ton1[ch[i]-'a']; while(temp<n) { int cnt=0;memset(v,0,sizeof(v));memset(ton,0,sizeof(ton)); for(int i=1;i<=n;i++) tmp[i]=rank[i]*n+rank[i+temp]; for(int i=1+temp;i<=n+temp;i++) ton[rank[i]].push_back(i-temp); for(int i=0;i<=n;i++) if(ton[i].size()!=0) for(int j=0,siz=ton[i].size();j<siz;j++) v[rank[ton[i][j]]].push_back(ton[i][j]); for(int i=1;i<=n;i++) for(int j=0,siz=v[i].size();j<siz;j++) { if(j==0||(rank[v[i][j]]!=rank[v[i][j-1]]||rank[v[i][j]+temp]!=rank[v[i][j-1]+temp])) cnt++; rank1[v[i][j]]=cnt; } for(int i=1;i<=n;i++) rank[i]=rank1[i]; temp*=2; } for(int i=1;i<=n;i++) sa[rank[i]]=i; temp=0; for(int i=1;i<=n;i++) { if(temp) temp--; int now=sa[rank[i]-1]; while(1) { if(i+temp>n||now+temp>n) break; if(ch[i+temp]==ch[now+temp]) temp++; else break; } h[rank[i]]=temp; } } //求某两个后缀的最长公共前缀 (RMQ) from 51 to 72 void solve0() { pre(); scanf("%d",&m); for(int i=1;i<=n;i++) mn[i][0]=min(h[i],h[i+1]); for(int j=1;(1<<j)<=n;j++) for(int i=1;i<=n;i++) { mn[i][j]=min(mn[i][j-1],mn[i+(1<<(j-1))][j-1]); if(i+(1<<j)>n) break; } while(m--) { int fr,to;scanf("%d%d",&fr,&to); if(fr==to) { printf("%d\n",n-fr+1); continue; } fr=rank[fr],to=rank[to]; if(fr>to) swap(fr,to);fr++; if(fr==to) { printf("%d\n",h[fr]); continue; } int cnt=to-fr-1,ans=min(mn[fr][num[cnt]],mn[to-(1<<num[cnt])][num[cnt]]); printf("%d\n",ans); } } //不可重叠最长重复子串 (pku1743) from 74 to 97 bool Jud1(int k) { int mn=2147483647,mx=0; for(int i=1;i<=n;i++) { if(h[i]>=k) { mn=min(mn,sa[i]); mx=max(mx,sa[i]); } else { mn=sa[i]; mx=sa[i]; } if(mx-mn>=k) return true; } return false; } void solve1() { pre(); int l=0,r=n; while(l!=r) { int k=(l+r)/2,jud=Jud1(k); if(jud==1&&l==r-1) { if(Jud1(r)) { printf("%d\n",r); return ; } else { printf("%d\n",l); return ; } } if(jud==0) r=k; else l=k; } printf("%d\n",l); } //可重叠的k次最长重复子串 (pku3261) from 99 to 123 bool Jud2(int len,int k) { int cnt=0; for(int i=1;i<=n;i++) { if(h[i]>=len) cnt++; else cnt=1; if(cnt>=k) return true; } return false; } void solve2() { pre(); int l=0,r=n,k; scanf("%d",&k); while(l!=r) { int len=(l+r)/2,jud=Jud2(len,k); if(jud==1&&l==r-1) { if(Jud2(r,k)) { printf("%d\n",r); return ; } else { printf("%d\n",l); return ; } } if(jud==0) r=len; else l=len; } printf("%d\n",l); } //可重叠最长重复子串 from 125 to 131 void solve3() { pre(); int ans=0; for(int i=1;i<=n;i++) ans=max(ans,h[i]); printf("%d\n",ans); } //不相同的子串的个数 (spoj694&&spoj705) from 133 to 139 void solve4() { pre(); int ans=0; for(int i=1;i<=n;i++) ans+=n+1-sa[i]-h[i]; printf("%d\n",ans); }//连续重复子串 (原串是由一个子串重复而成) (pku2406) from 141 to 157 void solve5() { pre(); int Tmp[Mx];memset(Tmp,0x3f,sizeof(Tmp)); for(int i=rank[1]-1;i>=1;i--) Tmp[i]=min(Tmp[i+1],h[i+1]); for(int i=rank[1]+1;i<=n;i++) Tmp[i]=min(Tmp[i-1],h[i+1]); Tmp[rank[1]]=0; for(int k=1;k<=n;k++) //枚举子串长度 if(n%k==0) { if(Tmp[rank[k+1]]==n-k) { printf("%d\n",n/k); return ; } } } //重复次数最多的连续重复子串 (spoj687,pku3693) from 159 to 237 int Rank[Mx],Mn[Mx][50],H[Mx],Sa[Mx]; char Ch[Mx]; void clear() { memset(v,0,sizeof(v)); memset(ton,0,sizeof(ton)); memset(rank1,0,sizeof(rank1)); memset(tmp,0,sizeof(tmp)); memset(Ton,0,sizeof(Ton)); memset(ton1,0,sizeof(ton1)); } void pre1()//反着做一遍后缀数组 { clear(); for(int i=1;i<=n;i++) Ch[n-i+1]=Ch[i]; num[1]=1;temp=1; for(int i=1;i<=n;i++) Ton[Ch[i]-'a']=1; for(int i=0,lst=0;i<26;i++) if(Ton[i]==1) ton1[i]=ton1[lst]+1,lst=i; for(int i=1;i<=n;i++) Rank[i]=ton1[Ch[i]-'a']; while(temp<n) { int cnt=0;memset(v,0,sizeof(v));memset(ton,0,sizeof(ton)); for(int i=1;i<=n;i++) tmp[i]=Rank[i]*n+Rank[i+temp]; for(int i=1+temp;i<=n+temp;i++) ton[Rank[i]].push_back(i-temp); for(int i=0;i<=n;i++) if(ton[i].size()!=0) for(int j=0,siz=ton[i].size();j<siz;j++) v[Rank[ton[i][j]]].push_back(ton[i][j]); for(int i=1;i<=n;i++) for(int j=0,siz=v[i].size();j<siz;j++) { if(j==0||(Rank[v[i][j]]!=Rank[v[i][j-1]]||Rank[v[i][j]+temp]!=Rank[v[i][j-1]+temp])) cnt++; rank1[v[i][j]]=cnt; } for(int i=1;i<=n;i++) Rank[i]=rank1[i]; temp*=2; } for(int i=1;i<=n;i++) Sa[Rank[i]]=i; temp=0; for(int i=1;i<=n;i++) { if(temp) temp--; int now=Sa[Rank[i]-1]; while(1) { if(i+temp>n||now+temp>n) break; if(Ch[i+temp]==Ch[now+temp]) temp++; else break; } H[Rank[i]]=temp; } } void solve6() { pre();pre1(); for(int i=1;i<=n;i++) mn[i][0]=min(h[i],h[i+1]),Mn[i][0]=min(H[i],H[i+1]); for(int j=1;(1<<j)<=n;j++) for(int i=1;i<=n;i++) { mn[i][j]=min(mn[i][j-1],mn[i+(1<<(j-1))][j-1]); Mn[i][j]=min(Mn[i][j-1],Mn[i+(1<<(j-1))][j-1]); if(i+(1<<j)>n) break; } int ans=1; for(int l=1;l<=n;l++) { for(int i=l;i<=n;i+=l) { int fr,to;temp=l; fr=rank[i-l],to=rank[i]; if(fr>to) swap(fr,to);fr++; int cnt=to-fr-1;temp+=min(mn[fr][num[cnt]],mn[to-(1<<num[cnt])][num[cnt]]); fr=Rank[i-l],to=Rank[i]; if(fr>to) swap(fr,to);fr++; cnt=to-fr-1;temp+=min(Mn[fr][num[cnt]],Mn[to-(1<<num[cnt])][num[cnt]]); ans=max(ans,(temp/l)+1); } } printf("%d\n",ans); } //最长公共子串 (pku2774) from 239 to 252 void solve7() { ch[++n]='$';n++; scanf("%d",&m); for(int i=1;i<=m;) { scanf("%c",&ch[n]); if(ch[n]>='a'&&ch[n]<='z') i++,n++; } pre();int ans=0; for(int i=2;i<=n;i++) if((sa[i]<=n&&sa[i-1]>n+1)||(sa[i-1]<=n&&sa[i]>n+1)) ans=max(ans,h[i]); printf("%d\n",ans); }int main() { scanf("%d",&n); for(int i=1;i<=n;) { scanf("%c",&ch[i]); if(ch[i]>='a'&&ch[i]<='z') i++; } return 0; }
二、后缀树(通常用不到,略)
三、后缀自动机
如:字符串aabbabd,建出自动机如下图:
每一个后缀都是从s走到终结状态(红色节点)
copy from hihocoder
SAM的States
小Hi:这一节我们将介绍给定一个字符串S,如何确定S对应的SAM有哪些状态。首先我们先介绍一个概念 子串的结束位置集合 endpos。对于S的一个子串s,endpos(s) = s在S中所有出现的结束位置集合。还是以S="aabbabd"为例,endpos("ab") = {3, 6},因为"ab"一共出现了2次,结束位置分别是3和6。同理endpos("a") = {1, 2, 5}, endpos("abba") = {5}。
小Hi:我们把S的所有子串的endpos都求出来。如果两个子串的endpos相等,就把这两个子串归为一类。最终这些endpos的等价类就构成的SAM的状态集合。例如对于S="aabbabd"
状态 | 子串 | endpos |
---|---|---|
S | 空串 | {0,1,2,3,4,5,6} |
1 | a | {1,2,5} |
2 | aa | {2} |
3 | aab | {3} |
4 | aabb,abb,bb | {4} |
5 | b | {3,4,6} |
6 | aabba,abba,bba,ba | {5} |
7 | aabbab,abbab,bbab,bab | {6} |
8 | ab | {3,6} |
9 | aabbabd,abbabd,bbabd,babd,abd,bd,d | {7} |
小Ho:这些状态恰好就是上面SAM图中的状态。
小Hi:没错。此外,这些状态还有一些美妙的性质,且等我一一道来。首先对于S的两个子串s1和s2,不妨设length(s1) <= length(s2),那么 s1是s2的后缀当且仅当endpos(s1)⊇endpos(s2),s1不是s2的后缀当且仅当endpos(s1) ∩ endpos(s2) = ∅。
小Ho:我验证一下啊... 比如"ab"是"aabbab"的后缀,而endpos("ab")={3,6},endpos("aabbab")={6},是成立的。"b"是"ab"的后缀,endpos("b")={3,4,6}, endpos("ab")={3,6}也是成立的。"ab"不是"abb"的后缀,endpos("ab")={3,6},endpos("abb")={4},两者没有交集也是成立的。怎么证明呢?
小Hi:证明还是比较直观的。首先证明s1是s2的后缀=>endpos(s1) ⊇ endpos(s2):既然s1是s2后缀,所以每次s2出现时s1以必然伴随出现,所以有endpos(s1) ⊇ endpos(s2)。再证明endpos(s1) ⊇ endpos(s2)=>s1是s2的后缀:我们知道对于S的子串s2,endpos(s2)不会是空集,所以endpos(s1) ⊇ endpos(s2)=>存在结束位置x使得s1结束于x,并且s2也结束于x,又length(s1) <= length(s2),所以s1是s2的后缀。综上我们可知s1是s2的后缀当且仅当endpos(s1) ⊇ endpos(s2)。s1不是s2的后缀当且仅当endpos(s1) ∩ endpos(s2) = ∅是一个简单的推论,不再赘述。
小Ho:我好像对SAM的状态有一些认识了!我刚才看上面的表格就觉得SAM的一个状态里包含的子串好像有规律。考虑到SAM中的一个状态包含的子串都具有相同的endpos,那它们应该都互为后缀?
小Hi:你观察力还挺敏锐的。下面我们就来讲讲一个状态包含的子串究竟有什么关系。上文提到我们把S的所有子串按endpos分类,每一类就代表一个状态,所以我们可以认为一个状态包含了若干个子串。我们用substrings(st)表示状态st中包含的所有子串的集合,longest(st)表示st包含的最长的子串,shortest(st)表示st包含的最短的子串。例如对于状态7,substring(7)={aabbab,abbab,bbab,bab},longest(7)=aabbab,shortest(7)=bab。
小Hi:对于一个状态st,以及任意s∈substrings(st),都有s是longest(st)的后缀。证明比较容易,因为endpos(s)=endpos(longest(st)),所以endpos(s) ⊇ endpos(longest(st)),根据我们刚才证明的结论有s是longest(st)的后缀。
小Hi:此外,对于一个状态st,以及任意的longest(st)的后缀s,如果s的长度满足:length(shortest(st)) <= length(s) <= length(longsest(st)),那么s∈substrings(st)。 证明也是比较容易,因为:length(shortest(st)) <= length(s) <= length(longsest(st)),所以endpos(shortest(st)) ⊇ endpos(s) ⊇ endpos(longest(st)), 又endpos(shortest(st)) = endpos(longest(st)),所以endpos(shortest(st)) = endpos(s) = endpos(longest(st)),所以s∈substrings(st)。
小Ho:这么说来,substrings(st)包含的是longest(st)的一系列连续后缀?
小Hi:没错。比如你看状态7中包含的就是aabbab的长度分别是6,5,4,3的后缀;状态6包含的是aabba的长度分别是5,4,3,2的后缀。
SAM的Suffix Links
小Hi:前面我们讲到substrings(st)包含的是longest(st)的一系列连续后缀。这连续的后缀在某个地方会“断掉”。比如状态7,包含的子串依次是aabbab,abbab,bbab,bab。按照连续的规律下一个子串应该是"ab",但是"ab"没在状态7里,你能想到这是为什么么?
小Ho:aabbab,abbab,bbab,bab的endpos都是{6},下一个"ab"当然也在结束位置6出现过,但是"ab"还在结束位置3出现过,所以"ab"比aabbab,abbab,bbab,bab出现次数更多,于是就被分配到一个新的状态中了。
小Hi:没错,当longest(st)的某个后缀s在新的位置出现时,就会“断掉”,s会属于新的状态。比如上例中"ab"就属于状态8,endpos("ab"}={3,6}。当我们进一步考虑"ab"的下一个后缀"b"时,也会遇到相同的情况:"b"还在新的位置4出现过,所以endpos("b")={3,4,6},b属于状态5。在接下去处理"b"的后缀我们会遇到空串,endpos("")={0,1,2,3,4,5,6},状态是起始状态S。
小Hi:于是我们可以发现一条状态序列:7->8->5->S。这个序列的意义是longest(7)即aabbab的后缀依次在状态7、8、5、S中。我们用Suffix Link这一串状态链接起来,这条link就是上图中的绿色虚线。
小Ho:原来如此。
小Hi:Suffix Links后面会有妙用,我们暂且按下不表。
SAM的Transition Function
小Hi:最后我们来介绍SAM的转移函数。对于一个状态st,我们首先找到从它开始下一个遇到的字符可能是哪些。我们将st遇到的下一个字符集合记作next(st),有next(st) = {S[i+1] | i ∈ endpos(st)}。例如next(S)={S[1], S[2], S[3], S[4], S[5], S[6], S[7]}={a, b, d},next(8)={S[4], S[7]}={b, d}。
小Hi:对于一个状态st来说和一个next(st)中的字符c,你会发现substrings(st)中的所有子串后面接上一个字符c之后,新的子串仍然都属于同一个状态。比如对于状态4,next(4)={a},aabb,abb,bb后面接上字符a得到aabba,abba,bba,这些子串都属于状态6。
小Hi:所以我们对于一个状态st和一个字符c∈next(st),可以定义转移函数trans(st, c) = x | longest(st) + c ∈ substrings(x) 。换句话说,我们在longest(st)(随便哪个子串都会得到相同的结果)后面接上一个字符c得到一个新的子串s,找到包含s的状态x,那么trans(st, c)就等于x。
小Ho:吼~ 终于把SAM中各个部分搞明白了。