后缀数组/后缀自动机 入门转载&板子

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 ;
}
板子1
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 ;
}
板子2

板子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 ;
}
板子1
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

板子2写成结构体形式,方便多个后缀自动机时调用。

后缀数组与后缀自动机的比较

转自【整理】如何选取后缀数组&&后缀自动机

  • 单个字符串问题                                                                  不等号是“优于”,&&是差不多(以下是个人感觉)

    • 1重复子串
      • 1,1 可交叉最长重复子串                              后缀自动机>=后缀数组                          都是基本题,但是前者代码稍短
      • 1,2 不可交叉最长重复子串                          后缀数组>=后缀自动机                          前者易于判断交叉;后者则需要记录每个状态所有出现的位置
      • 1,3 可交叉的k次最长重复子串                     后缀自动机>=后缀数组                          后者需+二分判定;前者无需判断,直接拓扑出每个状态的次数                     
    • 2子串个数问题
      • 2,1 不相同子串个数                                     后缀自动机&&后缀数组                          都是基本功能,易于实现。
    • 3循环子串问题
      • 3,1 求最小循环节                                         后缀数组,kmp                                           后缀自动机应该不行。
      • 3,2 重复次数最多的连续重复子串                后缀数组
  • 两个字符串串问题

    • 1公共子串问题 
      • 1,1 最长公共子串                                         后缀自动机&&后缀数组                          都是基本功能
    • 2子串个数问题
      • 2,1 特定长度的公共子串                              后缀自动机&&后缀数组                           二者的基本功能
  • 多个字符串问题

    • 1公共子串问题
      • 1,1 在k个字符串中的出现的最长子串           广义后缀自动机>=后缀数组(KMP也可以求多个串的最长公共字串)               (具体效率谁高取决于数据)
      • 1,2 在每个字符串中出现k次的最长子串       广义后缀自动机>=后缀数组
      • 1,3 在每个字符串中或反转后出现的最长子串 广义后缀自动机?后缀数组            
  • 其他

  • 最小表示法: 后缀自动机                
  • 最小循环节 :后缀数组
posted @ 2020-07-17 16:37  新之守护者  阅读(263)  评论(0编辑  收藏  举报