后缀数组

。_饼干妹妹 提交于 2020-01-19 02:50:17

后缀数组个人感觉的确有点复杂,看了挺久的,听说后缀数组是一种神仙操作,忘记在哪听到这个就学了一下

学习博客:https://www.cnblogs.com/victorique/p/8480093.html

什么是后缀数组

我们先看几条定义:

子串

在字符串s中,取任意i<=j,那么在s中截取从i到j的这一段就叫做s的一个子串

后缀

后缀就是从字符串的某个位置i到字符串末尾的子串,我们定义以s的第i个字符为第一个元素的后缀为suff(i)

后缀数组

把s的每个后缀按照字典序排序,

后缀数组sa[i]就表示排名为i的后缀的起始位置的下标

而它的映射数组rk[i]就表示起始位置的下标为i的后缀的排名

简单来说,sa表示排名为i的是啥,rk表示第i个的排名是啥

一定要记牢这些数组的意思,后面看代码的时候如果记不牢的话就绝对看不懂

后缀数组的思想

先说最暴力的情况,快排(n log n)每个后缀,但是这是字符串,所以比较任意两个后缀的复杂度其实是O(n),这样一来就是接近O(n^2 log n)的复杂度,数据大了肯定是不行的,所以我们这里有两个优化。

ps:本文中的^表示平方而不是异或

倍增

首先读入字符串之后我们现根据单个字符排序,当然也可以理解为先按照每个后缀的第一个字符排序。对于每个字符,我们按照字典序给一个排名(当然可以并列),这里称作关键字。

接下来我们再把相邻的两个关键字合并到一起,就相当于根据每一个后缀的前两个字符进行排序。想想看,这样就是以第一个字符(也就是自己本身)的排名为第一关键字,以第二个字符的排名为第二关键字,把组成的新数排完序之后再次标号。没有第二关键字的补零。

既然是倍增,就要有点倍增的样子。接下来我们对于一个在第i位上的关键字,它的第二关键字就是第(i+2)位置上的,联想一下,因为现在第i位上的关键字是suff(i)的前两个字符的排名,第i+2位置上的关键字是suff(i+2)的前两个字符的排名,这两个一合并,不就是suff(i)的前四个字符的排名吗?方法同上,排序之后重新标号,没有第二关键字的补零。同理我们可以证明,下一次我们要合并的是第i位和第i+4位,以此类推即可……

ps:本文中的“第i位”表示下标而不是排名。排名的话我会说“排名为i”

那么我们什么时候结束呢?很简单,当所有的排名都不同的时候我们直接退出就可以了,因为已经排好了。

显然这样排序的速度稳定在(log n)

基数排序

如果我们用快排的话,复杂度就是(n log^2 n) 还是太大。

这里我们用一波基数排序优化一下。在这里我们可以注意到,每一次排序都是排两位数,所以基数排序可以将它优化到O(n)级别,总复杂度就是(n log n)。

介绍一下什么是基数排序,这里就拿两位数举例

我们要建两个桶,一个装个位,一个装十位,我们先把数加到个位桶里面,再加到十位桶里面,这样就能保证对于每个十位桶,桶内的顺序肯定是按个位升序的,很好理解。

题目链接:https://www.luogu.org/recordnew/show/17637546

看代码:

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
using namespace std;
typedef long long LL;
const LL maxn=1e6+50;
char s[maxn];//存储输入的字符串
int y[maxn],x[maxn],c[maxn],sa[maxn],rk[maxn],height[maxn],wt[30];
int n,m;
void putout(int x)
{
    if(!x)
    {
        putchar(48);
        return ;
    }
    int l=0;
    while(x) wt[++l]=x%10,x/=10;
    while(l) putchar(wt[l--]+48);
}
void get_SA()
{
    for(int i=1;i<=n;i++) ++c[x[i]=s[i]];//以字符的ASCII码为下标
    //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 i=1;i<=n;i++) cout<<sa[i]<<" ";
//    cout<<endl;
//    for(int i=1;i<=n;i++) sa[c[x[i]]--]=i;

    //为什么要从后往前呢?  因为我们按照单个字符比较大小  那么很有可能会有相同的字符  那么这些
    //相同的字符怎么排名呢? 就先按照越在后的排名越往后的规则来

    //前面已经说过 sa[i]说的是排第i的谁
    /**
    上面的代码已经按照单个字符排好序了 其实也就是按照了每个后缀数组的第一关键字排好序了
    */
    for(int k=1;k<=n;k<<=1)//这里是倍增
    {
        int num=0;
        for(int i=n-k+1;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位以后
        //如果满足(sa[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];//第一关键字排名为1~i的数有多少个


        for(int i=n;i>=1;i--) sa[c[x[y[i]]]--]=y[i],y[i]=0;
//        for(int i=1;i<=n;i++) sa[c[x[y[i]]]--]=y[i],y[i]=0;

//        for(int i=1;i<=n;i++) cout<<sa[i]<<" ";
//        cout<<endl;
        //因为y的顺序是按照第二关键字的顺序来排的
        //第二关键字靠后的,在同一个关键字桶中排名越靠后
        //基数排序
        swap(x,y);
        //所以现在y[i] 存的是第i个元素的关键字

        //这里不用想太多 因为要生成新的x 时  要用到旧的   就把旧的复制下来  没别的意思
        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++) putout(sa[i]),putchar(' ');
    cout<<sa[1];
    for(int i=2;i<=n;i++)
    {
        cout<<" "<<sa[i];
    }
    cout<<endl;
}
int main()
{
//    cout<<(int)'0'<<endl;
//    cout<<(int)'z'<<endl;
    gets(s+1);//从1开始读入
    n=strlen(s+1);//得到字符串的长度
    m=122;//m表示字符个数  最小的数字0是48  最大的是字母z  是122
    get_SA();
    return 0;
}

 

 

下面是经常和后缀数组一起用的height数组:

个人感觉,上面说的一大堆,都是为heightheight数组做铺垫的,heightheight数组才是后缀数组的精髓、

先说定义

ii号后缀:从ii开始的后缀

lcp(x,y)lcp(x,y):字符串xx与字符串yy的最长公共前缀,在这里指xx号后缀与与yy号后缀的最长公共前缀

height[i]height[i]:lcp(sa[i],sa[i1])lcp(sa[i],sa[i−1]),即排名为ii的后缀与排名为i1i−1的后缀的最长公共前缀

H[i]H[i]:height[rak[i]]height[rak[i]],即ii号后缀与它前一名的后缀的最长公共前缀

 

性质:H[i]H[i1]1H[i]⩾H[i−1]−1

证明引自远航之曲大佬

 

update in 2019.3.28

在复习的时候我发现这里的证明有一个跳点,包括论文中的证明也有一点不严谨的地方

下面两处画红线的地方均没有证明"suffix(k+1)"与"i前一名的后缀之间的关系",实际上这两者之间的关系是:他们的lcp至少为h[i - 1] - 1。可以用反证法证明,在此不再赘述

 

能够线性计算height[]的值的关键在于h[](height[rank[]])的性质,即h[i]>=h[i-1]-1,下面具体分析一下这个不等式的由来。

我们先把要证什么放在这:对于第i个后缀,设j=sa[rank[i] – 1],也就是说j是i的按排名来的上一个字符串,按定义来i和j的最长公共前缀就是height[rank[i]],我们现在就是想知道height[rank[i]]至少是多少,而我们要证明的就是至少是height[rank[i-1]]-1。

好啦,现在开始证吧。

首先我们不妨设第i-1个字符串(这里以及后面指的“第?个字符串”不是按字典序排名来的,是按照首字符在字符串中的位置来的)按字典序排名来的前面的那个字符串是第k个字符串,注意k不一定是i-2,因为第k个字符串是按字典序排名来的i-1前面那个,并不是指在原字符串中位置在i-1前面的那个第i-2个字符串。

这时,依据height[]的定义,第k个字符串和第i-1个字符串的公共前缀自然是height[rank[i-1]],现在先讨论一下第k+1个字符串和第i个字符串的关系。

第一种情况,第k个字符串和第i-1个字符串的首字符不同,那么第k+1个字符串的排名既可能在i的前面,也可能在i的后面,但没有关系,因为height[rank[i-1]]就是0了呀,那么无论height[rank[i]]是多少都会有height[rank[i]]>=height[rank[i-1]]-1,也就是h[i]>=h[i-1]-1。

第二种情况,第k个字符串和第i-1个字符串的首字符相同,那么由于第k+1个字符串就是第k个字符串去掉首字符得到的,第i个字符串也是第i-1个字符串去掉首字符得到的,那么显然第k+1个字符串要排在第i个字符串前面,要么就产生矛盾了。同时,第k个字符串和第i-1个字符串的最长公共前缀是height[rank[i-1]],那么自然第k+1个字符串和第i个字符串的最长公共前缀就是height[rank[i-1]]-1。

到此为止,第二种情况的证明还没有完,我们可以试想一下,对于比第i个字符串的字典序排名更靠前的那些字符串,谁和第i个字符串的相似度最高(这里说的相似度是指最长公共前缀的长度)?显然是排名紧邻第i个字符串的那个字符串了呀,即sa[rank[i]-1]。也就是说sa[rank[i]]和sa[rank[i]-1]的最长公共前缀至少是height[rank[i-1]]-1,那么就有height[rank[i]]>=height[rank[i-1]]-1,也即h[i]>=h[i-1]-1。

void get_height(int len)
{
    for(int i=1;i<=len;i++)
    {
        rk[sa[i]]=i;
    }
    int k=0;
    for(int i=1;i<=len;i++)
    {
        if(rk[i]==1) continue;
        if(k) --k;
        int j=sa[rk[i]-1];
        while(j+k<=len&&i+k<=len&&s[i+k]==s[j+k]) ++k;
        height[rk[i]]=k;
    }
    return ;
}

 

 

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