后缀数组总结

后缀数组是一种处理字符串的算法。
它可以将所有后缀按字典序排序,并求出任意2个后缀的最长公共前缀。
首先将所有后缀排序,因为后缀有这样的性质:任意一个后缀都可以拆分成另一个后缀和一个子串,且一个子串也可以拆分成其他子串,所以可以采用倍增算法对所有后缀进行排序,方法如下:(分若干步)
第k步的处理如下:
首先,根据上一步的结果,得到每个位置开始,长度为\(2^{k-1}\)的子串的排名。
然后,把长度为\(2^{k-1}\)的子串拼在一起,得到长度为\(2^{k}\)的子串。
通过之前的排名,将长度为\(2^{k-1}\)的子串,变为一个数字(原理类似离散化)。
这样,长度为\(2^{k}\)的的子串,就变成了两个数字。
这样,通过排序,就能求出从每个位置开始,长度为\(2^{k}\)的子串的排名。
如下图:

因为需要排序的数字的最大值不会超过n,所以可以采用基数排序,时间复杂度\(O(nlogn)\)

后缀数组代码:

#include <stdio.h>
#include <string.h>
char zf[1000010];
int js[1000010]={0};
int x[1000010],y[1000010];
int sa[1000010];
int main()
{
    scanf("%s",zf);
    int n,m;
    for(n=0;zf[n]!=0;n++)
        js[x[n]=zf[n]]+=1;//基数排序,同时求出字符串长度
    for(int i='0';i<='z';i++)
        js[i]+=js[i-1];;//基数排序,求前缀和
    for(int i=n-1;i>=0;i--)
        sa[--js[x[i]]]=i;//放回数组
    m='z';
    for(int mi=1;mi<=n;mi=(mi<<1))
    {
        int s=0;
        for(int i=n-mi;i<n;i++)
            y[s++]=i;//处理出第二关键字为空的位置
        for(int i=0;i<n;i++)//从小到大,枚举第二关键字的位置
        {
            if(sa[i]>=mi)//此位置存在对应的第一关键字
                y[s++]=sa[i]-mi;//保存位置
        }
        //此时,y中储存的是按照第二关键字从小到大排序后,第一关键字的位置
        
        for(int i=1;i<=m;i++)
            js[i]=0;
        for(int i=0;i<n;i++)
            js[x[i]]+=1;
        for(int i=1;i<=m;i++)
            js[i]+=js[i-1];
        for(int i=n-1;i>=0;i--)
            sa[--js[x[y[i]]]]=y[i];
        //对第一关键字进行基数排序
        
        m=1;
        y[sa[0]]=1;
        for(int i=1;i<n;i++)
        {
            if(x[sa[i]]!=x[sa[i-1]]||x[sa[i]+mi]!=x[sa[i-1]+mi])//与上一个不相同
                m+=1;
            y[sa[i]]=m;
        }
        //处理排名
        
        if(m==n)//所有排名都出现过,算法结束
            break;  
        for(int i=0;i<n;i++)//将排名赋值回原来的数组
            x[i]=y[i];
    }
    for(int i=0;i<n;i++)
        printf("%d ",sa[i]+1);
    return 0;
}

height数组:

很多时候,只有一个sa数组,能做的事情并不多,我们通常还需要一个height数组,表示排名相邻的两个后缀的最长公共前缀。
暴力求是\(O(n^2)\)的。
考虑优化:
设h(i)表示从i开始的后缀和它上一排名的最长公共前缀。
有如下结论成立:\(h(i+1)\geq h(i)-1\)
证明:
分两种情况:

  1. h(i)>0,设i的上一排名为j,说明后缀i的第一个字符和后缀j的第一个字符相等,后缀j<后缀i,所以后缀j+1<后缀i+1(就是都去掉第一个字符后仍然小于)。后缀j+1和后缀i+1的最长公共前缀长度是h(i)-1,并且在同大于或小于的i情况下,排名越接近,h越大。所以h(i+1)至少是h(i)-1。
  2. h(i)=0。此时,h(i)-1为-1。由于h一定为非负整数,所以h(i+1)一定>-1。
    证明完毕。
    然后,通过height数组,还可以求得任意两个后缀的最长公共前缀。
    如下图:

就是从i到j,最大的变化。
就是求\(RMQ(h(ra[a]+1),h(ra[b]))\)

代码:

        for(int i=0,h=0;i<n;i++)
        {
            if(ra[i]==0)
                continue;
            if(h>0)
                h-=1;
            int j=sa[ra[i]-1];
            while(zf[i+h]==zf[j+h])
                h+=1;
            hei[ra[i]]=h;
        }

例题:

NOI2016优秀的拆分
枚举长度L,然后放置关键点,求相邻关键点的lcp,lcs,然后差分。
复杂度\(O(nlogn)\)
代码:

#include <stdio.h>
#include <string.h>
#define ll long long
int lo[30010],N;
char ch[30010];
int cf1[30010],cf2[30010];
struct SA
{
    int sa[30010],x[60010],y[30010],sl[30010],ra[30010],hei[30010],zx[15][30010];
    char zf[30010];
    void getsa(int n)
    {
        zf[n]=0;
        for(int i=0;i<=n+n;i++)
            x[i]=0;
        for(int i=0;i<='z';i++)
            sl[i]=0;
        for(int i=0;i<n;i++)
            sl[x[i]=zf[i]]+=1;
        for(int i=1;i<='z';i++)
            sl[i]+=sl[i-1];
        for(int i=n-1;i>=0;i--)
            sa[--sl[x[i]]]=i;
        int m='z';
        for(int mi=1;mi<=n;mi*=2)
        {
            int s=0;
            for(int i=n-mi;i<n;i++)
                y[s++]=i;
            for(int i=0;i<n;i++)
            {
                if(sa[i]>=mi)
                    y[s++]=sa[i]-mi;
            }
            for(int i=1;i<=m;i++)
                sl[i]=0;
            for(int i=0;i<n;i++)
                sl[x[i]]+=1;
            for(int i=1;i<=m;i++)
                sl[i]+=sl[i-1];
            for(int i=n-1;i>=0;i--)
                sa[--sl[x[y[i]]]]=y[i];
            m=1;
            for(int i=0;i<n;i++)
            {
                if(i!=0&&(x[sa[i]]!=x[sa[i-1]]||x[sa[i]+mi]!=x[sa[i-1]+mi]))
                    m+=1;
                y[sa[i]]=m;
            }
            if(m==n)
                break;
            for(int i=0;i<n;i++)
                x[i]=y[i];
        }
        for(int i=0;i<n;i++)
            ra[sa[i]]=i;
        for(int i=0,h=0;i<n;i++)
        {
            if(ra[i]==0)
                continue;
            if(h>0)
                h-=1;
            int j=sa[ra[i]-1];
            while(zf[i+h]==zf[j+h])
                h+=1;
            hei[ra[i]]=h;
        }
        for(int i=1;i<n;i++)
            zx[0][i]=hei[i];
        for(int i=1;i<=lo[n];i++)
        {
            for(int j=1;j<n;j++)
            {
                if(j+(1<<i)-1>=n)
                    break;
                zx[i][j]=zx[i-1][j+(1<<(i-1))];
                if(zx[i-1][j]<zx[i][j])
                    zx[i][j]=zx[i-1][j];
            }
        }
    }
    int RMQ(int l,int r)
    {
        int i=lo[r-l+1];
        int jg=zx[i][r-(1<<i)+1];
        if(zx[i][l]<jg)
            jg=zx[i][l];
        return jg;
    }
};
SA ch1,ch2;
int getlcp(int a,int b)
{
    if(ch1.ra[a]>ch1.ra[b])
    {
        int t=a;
        a=b;
        b=t;
    }
    return ch1.RMQ(ch1.ra[a]+1,ch1.ra[b]);
}
int getlcs(int a,int b)
{
    a=N-1-a;
    b=N-1-b;
    if(ch2.ra[a]>ch2.ra[b])
    {
        int t=a;
        a=b;
        b=t;
    }
    return ch2.RMQ(ch2.ra[a]+1,ch2.ra[b]);
}
void yucl()
{
    lo[1]=0;
    for(int i=2;i<=N;i++)
        lo[i]=lo[i>>1]+1;
    ch1.getsa(N);
    ch2.getsa(N);
}
int main()
{
    int T;
    scanf("%d",&T);
    while(T--)
    {
        scanf("%s",ch);
        N=strlen(ch);
        for(int i=0;i<N;i++)
        {
            ch1.zf[i]=ch2.zf[N-1-i]=ch[i];
            cf1[i]=cf2[i]=0;
        }
        yucl();
        for(int l=1;l<=N;l++)
        {
            for(int i=0;i+l<N;i+=l)
            {
                int j=i+l;
                int x=getlcp(i,j),y=getlcs(i,j);
                if(x>l)
                    x=l;
                if(y>l)
                    y=l;
                if(x+y>l)
                {
                    int L=i+1-y,R=i+x-l;
                    cf1[L]+=1;
                    cf1[R+1]-=1;
                    cf2[L+l+l]+=1;
                    cf2[R+l+l+1]-=1;
                }
            }
        }
        ll jg=0;
        int he1=0,he2=0;
        for(int i=0;i<N;i++)
        {
            he1+=cf1[i];
            he2+=cf2[i];
            jg+=(ll)he1*he2;
        }
        printf("%lld\n",jg);
    }
    return 0;
}

应用:

  1. 二分/枚举长度,然后划分为若干区间
  2. 统计问题可以转化为RMQ之和,可以枚举最小值,然后单调栈
  3. 循环串,连续重复串可以枚举长度,然后放置关键点,使用lcp+lcs等。
posted @ 2019-07-25 19:36  lnzwz  阅读(200)  评论(0编辑  收藏  举报