学习笔记:后缀数组

upd.学了后缀自动机,感觉自己已经真香了:“得想办法放下我无敌的SAM”

0、闲扯

我学后缀数组的时候问过lzy一个问题,大概是一段代码的含义

lzy:"你会后缀自动机吗?"

我:???

果然lzy太强了啊


1、一些定义

\(sa[i]:\)字典序从小到大排名为i的后缀的位置

\(rnk[i]:\)以位置i开头的后缀的排名(与sa互逆)

\(tp[i]:\)基数排序辅助数组,第二关键字(第二关键字是什么后面会讲)排名为i的后缀的位置

\(tax[i]:\)基数排序辅助数组,一个桶

\(lcp(x,y):\)以位置x开头的后缀和以位置y开头的后缀的最长公共前缀

\(heght[i]:lcp(sa[i-1],sa[i])\)

\(h[i]:height[rnk[i]]\)

关于基数排序,看这里


2、后缀排序

后缀排序的方法,常见的有倍增/DC3/SA-IS,倍增是\(O(nlogn)\)的,DC3和SA-IS都是\(O(n)\)的,但很难写 其实只是你不会吧,所以这里只介绍倍增。

说到倍增,就不得不上那张经典的思路图:

20160205125603928

可以发现一个从\(i\)开始,长度为\(2x\)的字字符串(记为\(S\)),是由从\(i\)开始,长度为\(x\)的字符串(记为\(S1\))和一个从\(i+x\)开始,长度为\(x\)的字符串组成的(记为\(S2\))。

此时\(S1\)的排名为\(S\)的第一关键字(图中x),\(S2\)的排名为\(S\)的第二关键字(图中y),得到\(S\)对应的二元组\((x,y)\)。再对\(n\)个这样的二元组进行排序得到新一轮的\(sa\),进而求出\(rnk\)。这么做正确性显然。

如果排序用快排的话,时间复杂度为\(O(nlogn^2)\),不如写个哈希。考虑到只是对二元组进行排序,基数排序的时间复杂度为\(O(n)\),时间复杂度为\(O(nlogn)\),非常优秀。

上一下基数排序的代码

void Qsort(){
    int i;
    for(i=0;i<=sz;++i) tax[i]=0;
    //清空桶,桶的大小为上轮的排名的个数
    for(i=1;i<=l;++i) tax[rnk[i]]++;
    //记录第一关键字
    for(i=1;i<=sz;++i) tax[i]+=tax[i-1];
    //前缀和,用于快速定位
    for(i=l;i>=1;--i) sa[tax[rnk[tp[i]]]--]=tp[i];
    //从大到小枚举第二关键字,再定位回第一关键字,得出排名
}

然后是整个后缀排序的代码:

void Qsort(){
    int i;
    for(i=0;i<=sz;++i) tax[i]=0;
    for(i=1;i<=l;++i) tax[rnk[i]]++;
    for(i=1;i<=sz;++i) tax[i]+=tax[i-1];
    for(i=l;i>=1;--i) sa[tax[rnk[tp[i]]]--]=tp[i];
}

inline bool cmp(int x,int y,int w){ return (tp[x]==tp[y])&&(tp[x+w]==tp[y+w]); }//二元组相同,排名相同

void SufSort(){
    int i,x;
    sz=75;
    for(i=1;i<=l;++i) rnk[i]=s[i]-'0'+1,tp[i]=i;
    Qsort();
    for(x=1;x<=l;x<<=1){
        int num=0; 
        for(i=l-x+1;i<=l;++i) tp[++num]=i;
        //没有第二关键字
        for(i=1;i<=l;++i) if(sa[i]>x) tp[++num]=sa[i]-x;
        //从i-x开始的后缀的第二关键字为从i开始的后缀的第一关键字,联系tp和sa含义理解吧qwq
        Qsort();
        memcpy(tp,rnk,sizeof(tp));
        //tp没啥用了,存上一轮的rnk以获得新一轮的rnk
        rnk[sa[1]]=1;
        for(i=2;i<=l;++i) rnk[sa[i]]=rnk[sa[i-1]]+(!cmp(sa[i],sa[i-1],x));  
        if(rnk[sa[l]]==l) break;
        sz=rnk[sa[l]];
    } 
}

3、height数组

忘记\(height/h\)定义的请自行往回翻 = =

关于\(h\)数组,有\(h[i]\geq h[i-1]-1\)

证明引自远航之曲大佬(大佬博客好像挂了QwQ)

能够线性计算\(height[]\)的值的关键在于\(H[]\) \((height[rank[]])\)的性质,即\(h[i] \geq 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]\geq 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]]\geq height[rank[i-1]]-1\),也即\(h[i]\geq h[i-1]-1\)

然后就可以愉快地写出代码求啦

    void GetHeight(){
        int i,k=0;
        for(i=1;i<=l;++i){
            if(k) k--;
            int j=sa[rnk[i]-1];
            while(s[i+k]==s[j+k]) k++;
            height[rnk[i]]=f[rnk[i]][0]=k;
        }
    }

有了height可以干很多很多事,由于现在题做得不是很多还不能讲QwQ,题单和套路什么的先咕咕咕了


4、参考资料

五分钟搞懂后缀数组!后缀数组解析以及应用(附详解代码)-byYxuanwKeith

后缀数组详解-by 自为风月马前卒

后缀数组 学习笔记-by XMinh

转载于:https://www.cnblogs.com/PsychicBoom/p/10740774.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值