后缀数组/后缀自动机 入门转载&板子
在HihoCoder题库中搜索后缀,就有关于后缀数组以及后缀自动机的入门题目以及讲解。
后缀数组
五分钟搞懂后缀数组!后缀数组解析以及应用(附详解代码)
后缀数组 最详细讲解
int sa[N],ra[N],height[N]; int A[N],B[N],cntA[N],cntB[N],tsa[N]; //sa[i]按字典序排序后,第i个后缀在原串中的位置 //ra[i]原串中i~n的后缀,按字典序排序后的编号 //height[i] 按字典序排序后,第i个后缀与第i-1个后缀的最长公共前缀 //A,B进行基数排序的两个关键字,cntA,cntB进行基数排序的“桶”, //tsa[i]临时数组,按第二个关键字排序后的串 deque<int> q; void getsa(int n){ //一开始以串中单个单位作为第一关键字排序 for(int i=0;i<=100;i++) cntA[i]=0; for(int i=1;i<=n;i++) cntA[a[i]]++; for(int i=1;i<=n;i++) cntA[i]+=cntA[i-1]; for(int i=n;i>=1;i--) sa[cntA[a[i]]--]=i; ra[sa[1]]=1; for(int i=2;i<=n;i++){ ra[sa[i]]=ra[sa[i-1]]; if(a[sa[i]]!=a[sa[i-1]]) ra[sa[i]]++; } //倍增的思想 for(int l=1;ra[sa[n]]<n;l<<=1){ for(int i=0;i<=n;i++) cntA[i]=cntB[i]=0; //ra[i]作为第一关键字,ra[i+l]作为第二关键字 for(int i=1;i<=n;i++){ cntA[A[i]=ra[i]]++; cntB[B[i]=(i+l<=n ? ra[i+l] : 0)]++; } for(int i=1;i<=n;i++){ cntA[i]+=cntA[i-1]; cntB[i]+=cntB[i-1]; } for(int i=n;i>=1;i--) tsa[cntB[B[i]]--]=i; for(int i=n;i>=1;i--) sa[cntA[A[tsa[i]]]--]=tsa[i]; ra[sa[1]]=1; for(int i=2;i<=n;i++){ ra[sa[i]]=ra[sa[i-1]]; if(A[sa[i]]!=A[sa[i-1]]||B[sa[i]]!=B[sa[i-1]]) ra[sa[i]]++; } } //height[ra[i]]>=height[ra[i-1]]-1 for(int i=1,j=0;i<=n;i++){ if(j) j--; while(a[i+j]==a[sa[ra[i]-1]+j]) j++; height[ra[i]]=j; } return ; }
int a[N]; int sa[N],ra[N],height[N]; int cntr[N],tsa[N]; //cntr"桶",tsa[i]临时数组,不同情况意义不同 deque<int> q; //根据第一关键字ra[i]和以及按第二关键字[i+l]排好序的tsa进行基数排序 void sasort(int n,int m){ for(int i=0;i<=m;i++) cntr[i]=0; for(int i=1;i<=n;i++) cntr[ra[i]]++; for(int i=1;i<=m;i++) cntr[i]+=cntr[i-1]; for(int i=n;i>=1;i--) sa[cntr[ra[tsa[i]]]--]=tsa[i]; } void getsa(int n){ int m=100;//初始的数据大小,比如ascii码的话,一般为127 for(int i=1;i<=n;i++) ra[i]=a[i],tsa[i]=i; sasort(n,m); for(int l=1;l<=n;l<<=1){ int num=0; //[n-l+1,n]的数第二关键字为0,按第二关键字排序肯定是在前面 for(int i=n-l+1;i<=n;i++) tsa[++num]=i; // 如果sa[i]>l,那么它可以作为sa[i]-l的第二关键字 for(int i=1;i<=n;i++) if(sa[i]>l) tsa[++num]=sa[i]-l; sasort(n,m); //重复空间利用,用tsa暂时存下上一轮的ra用于比较求出新的ra memcpy(tsa,ra,(n+1)*sizeof(int)); ra[sa[1]]=1; for(int i=2;i<=n;i++){ ra[sa[i]]=ra[sa[i-1]]; if(tsa[sa[i]]!=tsa[sa[i-1]]||tsa[sa[i]+l]!=tsa[sa[i-1]+l]) ra[sa[i]]++; } m=ra[sa[n]]; if(m>=n) break; } for(int i=1,j=0;i<=n;i++){ if(j) j--; while(a[i+j]==a[sa[ra[i]-1]+j]) j++; height[ra[i]]=j; } return ; }
板子1为HihoCoder中的源码,而板子2为我自己根据网上的代码改写的。
板子1相对比较稳定,而板子2更省空间,运行也快一点。
后缀自动机
后缀自动机(SAM)学习笔记
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 ; }
struct Sam{ int size,last,len[N],link[N],trans[N][31]; Sam(){ size=last=1; } 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; } }A;
板子2写成结构体形式,方便多个后缀自动机时调用。
后缀数组与后缀自动机的比较
-
单个字符串问题 不等号是“优于”,&&是差不多(以下是个人感觉)
- 1重复子串
- 1,1 可交叉最长重复子串 后缀自动机>=后缀数组 都是基本题,但是前者代码稍短
- 1,2 不可交叉最长重复子串 后缀数组>=后缀自动机 前者易于判断交叉;后者则需要记录每个状态所有出现的位置
- 1,3 可交叉的k次最长重复子串 后缀自动机>=后缀数组 后者需+二分判定;前者无需判断,直接拓扑出每个状态的次数
- 2子串个数问题
- 2,1 不相同子串个数 后缀自动机&&后缀数组 都是基本功能,易于实现。
- 3循环子串问题
- 3,1 求最小循环节 后缀数组,kmp 后缀自动机应该不行。
- 3,2 重复次数最多的连续重复子串 后缀数组
- 1重复子串
-
两个字符串串问题
- 1公共子串问题
- 1,1 最长公共子串 后缀自动机&&后缀数组 都是基本功能
- 2子串个数问题
- 2,1 特定长度的公共子串 后缀自动机&&后缀数组 二者的基本功能
- 1公共子串问题
-
多个字符串问题
- 1公共子串问题
- 1,1 在k个字符串中的出现的最长子串 广义后缀自动机>=后缀数组(KMP也可以求多个串的最长公共字串) (具体效率谁高取决于数据)
- 1,2 在每个字符串中出现k次的最长子串 广义后缀自动机>=后缀数组
- 1,3 在每个字符串中或反转后出现的最长子串 广义后缀自动机?后缀数组
- 1公共子串问题
-
其他
- 最小表示法: 后缀自动机
- 最小循环节 :后缀数组
我太难了~给个三连吧,亲~~~