• 后缀数组


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

    学习博客: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:本文中的^表示平方而不是异或

    倍增

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

    havana1

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

    havana2

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

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

    havana3

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

    显然这样排序的速度稳定在(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 ;
    }
    当初的梦想实现了吗,事到如今只好放弃吗~
  • 相关阅读:
    Java输出错误信息与调试信息
    Java实现两个变量的互换(不借助第3个变量)
    Java用三元运算符判断奇数和偶数
    使用webpack-dev-server设置反向代理解决前端跨域问题
    springboot解决跨域问题(Cors)
    Spring boot集成swagger2
    Redis学习汇总
    【年终总结】2017年迟来的总结
    Springboot项目maven多模块拆分
    Maven实现多环境打包
  • 原文地址:https://www.cnblogs.com/caijiaming/p/10608902.html
Copyright © 2020-2023  润新知