(模板)倍增法求后缀数组

题目链接:https://www.luogu.com.cn/problem/P3809

题意:给定长为n的字符串s,将s的每个后缀字符串排序(升序),从小到大输出后缀字符串在s中出现的第一个位置。

思路:

  今天碰到一个要用后缀数组的题,于是来洛谷刷模板题,搞了一整天终于明白了板子怎么写。

  参考大佬的博客:https://xminh.github.io/2018/02/27/%E5%90%8E%E7%BC%80%E6%95%B0%E7%BB%84-%E6%9C%80%E8%AF%A6%E7%BB%86(maybe)%E8%AE%B2%E8%A7%A3.html

  求sa数组的复杂度为O(nlogn)(博客中提到求sa数组还有一种O(n)的D3C算法,但比如倍增容易理解,代码较难实现,所以暂时只学倍增法),求height数组的复杂度为O(n)。

AC code(含注释):

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;

const int maxn=1000005;
char s[maxn];
int y[maxn],x[maxn],c[maxn],sa[maxn],rk[maxn],height[maxn];
int n,m;

//倍增+基数排序求sa,O(nlogn)
void get_SA(){
    for(int i=1;i<=n;++i){
        x[i]=s[i];
        ++c[x[i]];
    }
    //c数组是桶,x[i]是第i个元素的第一关键字
    for(int i=2;i<=m;++i)
        c[i]+=c[i-1];
    //求c的前缀和,得到每个关键字最多在第几名
    for(int i=n;i>=1;--i)
        sa[c[x[i]]--]=i;
    for(int k=1;k<=n;k<<=1){
        int num=0;
        for(int i=n+1-k;i<=n;++i)
            y[++num]=i;
        //y[i]表示第二关键字排名为i的数,第一关键字的位置
        //第n-k+1到第n位没有第二关键字,排在最前面
        for(int i=1;i<=n;++i)
            if(sa[i]>k)
                y[++num]=sa[i]-k;
        //排名为i的数,在树组中是否在第k位以后
        //如果满足,那么它可以作为别人的第二关键字
        //就把它的第一关键字的位置添加进y就行了
        //所以i枚举的是第二关键字的排名,第二关键字靠前的先入队
        for(int i=1;i<=m;++i) c[i]=0;
        //初始化桶c
        for(int i=1;i<=n;++i)
            ++c[x[i]];
        //因为上一次循环已经算出这次的第一关键字了,所以直接加就行
        for(int i=2;i<=m;++i)
            c[i]+=c[i-1];
        //求c的前缀和,得到每个关键字最多在多少名
        for(int i=n;i>=1;--i)
            sa[c[x[y[i]]]--]=y[i],y[i]=0;
        //因为y的顺序是按照第二关键字的顺序来排的
        //第二关键字靠后的,在同一第一关键字桶中排名越靠后
        //基数排序
        swap(x,y);
        x[sa[1]]=1,num=1;
        for(int i=2;i<=n;++i)
            x[sa[i]]=(y[sa[i]]==y[sa[i-1]] && y[sa[i]+k]==y[sa[i-1]+k])? num:++num;
        //因为sa[i]已经排好序了,所以可以按排名枚举,生成下一次循环的第一关键字
        if(num==n) break;
        //如果所有的序号都不一样,那么排序完毕
        m=num;
        //这里不用原来的122了,因为有新的编号了
    }
    for(int i=1;i<=n;++i){
        if(i!=1) printf(" ");
        printf("%d",sa[i]);
    }
}

//O(n)
//height[i]=LCP(i,i-1)
//LCP为suff(sa[i])与suff(sa[j])的最长公共前缀
//即排在第i的字符串和排在第j的字符串的最长公共前缀
//hi[i]=height[rk[i]]
//即第i个字符串和排序后在它前面的字符串的最长公共前缀
void get_height(){
    int k=0;
    for(int i=1;i<=n;++i) rk[sa[i]]=i;
    for(int i=1;i<=n;++i){
        if(rk[i]==1) continue;
        //height[1]=0
        if(k) --k;
        //因为h[i]>=h[i-1]-1
        int j=sa[rk[i]-1];
        while(j+k<=n&&i+k<=n&&s[i+k]==s[j+k]) ++k;
        height[rk[i]]=k;
        //h[i]=height[rk[i]];
    }
}

int main(){
    scanf("%s",s+1);
    n=strlen(s+1);
    m=122;
    //ascii('z')=122,表示字符个数
    //第一次读入字符直接按照ascii码来
    get_SA();
    //get_height();
    return 0;
}

 

posted @ 2020-02-27 20:29  Frank__Chen  阅读(189)  评论(0编辑  收藏  举报