后缀数组

為{幸葍}努か 提交于 2020-03-26 02:19:45

前一段时间看了后缀数组,也是对着大佬们的博客研究了半天,有几个点理解了好久才明白(这就是弱者的世界吗_(:з」∠)_,再加上后来敲题时也遇到了一些坑,正好今天有时间所以写个博客来记录一下吧,方便日后写bug时查阅
本文算是自己的一篇简略的小笔记吧,如果想正经学习的话建议去看大佬们的博客哦,附上一个写的很好的链接:https://blog.csdn.net/yxuanwkeith/article/details/50636898

后缀数组求出来两个数组sa[i]rank[i]sa[i]表示按字典序排序排名第i的后缀的起始位置下标,rank[i]表示以第i位为起始位置的后缀的排名,两者互逆,还有一个神奇的数组height[i]它表示的是排名第i的后缀和排名第i-1的后缀的LCP,有了这三个数组就可以解决大部分问题了
求法的话就是利用倍增(还有一个更优秀的DC3算法,但是好像很复杂的样子就没有看_(:з」∠)_

首先要注意的一点就是该算法涉及的数组最好都以下标为1作为数组的第一位也就是起始位,(鬼知道为什么我敲第一道题的时候以0为起始位置怎么改怎么wa,第二天以1为起始位置重新敲了一遍就莫名其妙的a了...
实现原理概述:用str来表示整个字符串,我们先处理每个长度为1的子串str[i]对应的sarank的信息,然后利用str[i]str[i+1]的信息就可以更新str[i~i+1]的信息,再然后利用str[i~i+1]str[i+2~i+3]更新出str[i~i+3]的信息...如此反复,最后每个str[i~n]对应的sarank就得到了,排序的话因为是利用双关键字所以采用的较快的基数排序
height[i]的求法则是利用一个神奇的性质来加速:height[rank[i]]>=height[rank[i-1]]-1,具体证明没有看,以后看了再补上吧(当然很大概率没有“以后”了2333

偷度娘一张经典图XD

直接上代码然后解释一些不太好懂的点吧,这也是本篇博客的目的所在

const int siz_n=100010;
char str[siz_n];                //str[i]是存字符串的数组;
int rnk[siz_n],bkt[siz_n];      //rnk[i]就是上述的rank[i],fir[i]是sa[i],lcp[i]是height[i],因为个人习惯给他们改了个名
int fir[siz_n],sec[siz_n];      //fir[i](即sa[i])为第一关键字,sec[i]为第二关键字
int lcp[siz_n];                 //用桶排来实现基数排序,bkt[i]就是桶

void resort(int n,int m)                                       //n为字符串长度,m为桶的容量
{
    for(int i=1;i<=m;i++) bkt[i]=0;                            //清空桶
    for(int i=1;i<=n;i++) bkt[rnk[i]]++;                       //将每个子串的rank信息装桶
    for(int i=1;i<=m;i++) bkt[i]+=bkt[i-1];                    //求前缀和
    for(int i=n;i>=1;i--) fir[bkt[rnk[sec[i]]]--]=sec[i];      //这里是最绕的,解释一下:因为从n到1遍历,sec[i]就是第二关键字的排名最靠后的下标
}                                                              //那么rnk[sec[i]]就是这个排名最靠后的下标的排名,放到这个语境中就可以理解为“桶的编号”
                                                               //bkt之前是求过前缀和的,所以bkt[rnk[sec[i]]]就是这个子串的正确排名排名
                                                               //那么fir[bkt[rnk[sec[i]]]--]=sec[i]的含义就显而易见了
void solve(int n)
{
    for(int i=1;i<=n;i++) rnk[i]=str[i],fir[i]=i;                  //这一步是初始化的,理解了算法含义之后就能看懂了
    for(int l=0,m=127,p;l<n;l=l?l*2:1)                             //倍增l,l就是两个相邻小段子串的长度,更新完的子串长度为2*l
    {
        int top=0;
        for(int i=n-l+1;i<=n;i++) sec[++top]=i;                    //或许是写法问题吧(明明是单纯的菜,基数排序这一部分是看的最久的
        for(int i=1;i<=n;i++) if(fir[i]>l) sec[++top]=fir[i]-l;    //上一行和这一行就算是把第二关键字的先后顺序存在sec[i]里,fir[i]存的信息不会受影响
        resort(n,m);                                               //第一关键字通过resort()函数进行排序来更新fir[i]
        swap(sec,rnk);                                             //sec[i]参与完基数排序就没用了(工具人,所以这里让它来暂时存一下rank方便一会根据sa更新rank数组的值
        rnk[fir[1]]=p=1;                                           //按照名次依次更新rank值,p是当前排到第几名了
        for(int i=2;i<=n;i++)                                      //不同子串的rank允许并列,因为并列了基数排序时它们才会进到同一个桶里
        if(sec[fir[i]]==sec[fir[i-1]]&&sec[fir[i]+l]==sec[fir[i-1]+l]) rnk[fir[i]]=p;
        else rnk[fir[i]]=++p;                                      //比较当前子串的两个关键字和它前一名子串的俩关键字一不一样,一样就是并列,名次也是p,反之++p
        m=p;                                                       //更新m,桶的容量
    }
    for(int i=1,k=0;i<=n;i++)                                      //按照神奇性质求height[i],不多解释
    {
        if(k) k--;
        while(str[i+k]==str[fir[rnk[i]-1]+k]) k++;
        lcp[rnk[i]]=k;
    }
}

int main()
{
    scanf("%s",str+1);
    int len=strlen(str+1);
    solve(len);
    return 0;
}

有了这几个数组再结合st表之类的东西就可以愉快的a题啦

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!