后缀数组-sa-SuffixArray学(复)习小记

功能

O(NlogN) O ( N l o g N ) 的时间将某个串的后缀排序,并利用这个做神奇的东西。

思想(倍增法)

sa[i],第i小后缀的起点
rk[i],以i为起点的后缀排第几。
注意要保证在排序过程中,内容相同则rk相同

假设我们已经将每个后缀的前k个字符排序,然后显然可以利用k个字符的排名去得到k*2个字符的排名,相当于多增加一个关键字。(就是倍增的思想)

其中方便实现的细节就是利用了一个cnt数组。
记每个位置的第一关键字key1与第二关键字key2。
首先保证key1相同的成块。
比如说cnt[5]表示key1=1的个数,我们将cnt做一个前缀和,则cnt[5]就是1..5的总个数。

在新的sa数组中,
key1=5,key2最大的在cnt[5]
key1=5,Key2次大的在cnt[5]-1
以此类推,我们只要保证以key2从大到小顺序插入,即可得到新的sa,不难由此算出新的rk.

具体流程

上代码好说话,我的板子是自己打的和网上的标可能有不一样,慎co.

排序均为字典序。也就是“小到大 ”

#include 
#include 
#include 
#include 
#define max(a,b) ((a)>(b)?(a):(b))
using namespace std;
typedef long long ll;

const int N=2e5+10;
char c[N];
int height[N],cnt[N],sa[N],rk[N],n,nsa[N],nrk[N];

ll ans;
void makesa() {
    //保证内容相同则rk相同

    for (int i=1; i<=n; i++) cnt[c[i]]++,rk[i]=c[i];
    for (int i=1; i<=127; i++) cnt[i]+=cnt[i-1];
    for (int i=1; i<=n; i++) sa[cnt[rk[i]]--]=i;

    for (int len=1; len*2<=n; len*=2) {
        memset(cnt,0,sizeof cnt);
        for (int i=1; i<=n; i++) cnt[rk[i]]++;
        for (int i=1; i<=max(n,127); i++) cnt[i]+=cnt[i-1];

        for (int i=n; i>0; i--) { //从大到小枚举key2
            if (sa[i] - len > 0) { //开头有一些做不了key2的
                int x=sa[i]-len;
                nsa[cnt[rk[x]]]=x;
                cnt[rk[x]]--;
            }
        }

        for (int i=n; i>n-len; i--) { //key2为空最后进
            nsa[cnt[rk[i]]]=i;
            cnt[rk[i]]--;
        }

        memcpy(sa,nsa,sizeof sa);
        for (int i=1; i<=n; i++) {
            nrk[sa[i]]=nrk[sa[i-1]] + (rk[sa[i]]!=rk[sa[i-1]] || rk[sa[i]+len]!=rk[sa[i-1]+len]);
            //这里注意内容完全一样,rk应该相同。
        }
        memcpy(rk,nrk,sizeof rk);
    }

    int last=0;
    for (int i=1; i<=n; i++) {
        if (rk[i]==1) last=0; else {
            for (;c[sa[rk[i]]+last]==c[sa[rk[i]-1]+last]; ) 
                last++;
            height[rk[i]]=last;
            if (last) last--;
        }
    }
}
int main() {
    freopen("1598.in","r",stdin);
    scanf("%s",c+1); n=strlen(c+1);
    makesa();
    for (int i=1; i<=n; i++) {
        ans+=max(0,height[i+1] - height[i]);
    }
    cout<

实现细节

cnt前缀和枚举到的位置(cnt数组)的大小应该为max(n,单个字符编号的上界)
空间记得要开到两倍,不然容易爆数组。

O(n)最长公共前缀lcp (Height)

设Lcp[i]=sa[i-1]与sa[i]的最长公共前缀。

先求1..n这个后缀的lcp即lcp[rk[1]],再求2..n这个后缀的lcp…
为什么这样是O(n)的呢?
假设我们已知1..n的lcp即lcp[rk[1]],设sa[rk[1]]=j,也就是j..n这个后缀。
那么我们lcp[1]也就是1..n与j..n的lcp.
都去掉第一个字符,对应着2..n与j+1..n这两个后缀。显然他们的lcp一定大于等于lcp[rk[1]]-1
注意到j+1..n一定排在2..n之前(在首字母相同的假设下),那么2..n与sa[rk[2]-1]的lcp也一定大于等于lcp[rk[1]]-1
由此可得 lcp[rk[2]]>=lcp[rk[1]]1 l c p [ r k [ 2 ] ] >= l c p [ r k [ 1 ] ] − 1
推广可得 lcp[rk[i+1]]>=lcp[rk[i]]1 l c p [ r k [ i + 1 ] ] >= l c p [ r k [ i ] ] − 1 ,因此从lcp[rk[i]]-1开始枚举检验。

注意到我们用了一个1..n与j..n首字母相同的假设,假如无这个假设,那么lcp[rk[i]]=0。
不妨碍我们开始检验,注意将-1变为0即可。

这样的话,发现长度开始为0,每次减1,最多增到n,则势能为O(n).因此总时间复杂度为O(n)

    for (int i=1; i<=le; i++) {
        if (rk[i]!=1) {
            hei[rk[i]]=hei[rk[i-1]]-1;
            while (s[i+hei[rk[i]]] == s[sa[rk[i]-1] + hei[rk[i]]])
                hei[rk[i]]++;
        }
    }

应用

1.两后缀间的最长公共前缀

实质为排序后两后缀之间的lcp的最小值,由传递性可得。

2.可重叠最长重复子串

等价与求任意两个后缀数组的最长公共前缀的max,也就是取一个最大的lcp即可,理由略去。

3.不可重叠最长重复子串

设答案为k,以每一个小于k的lcp为界限,将sa分组。
由1得,长度有希望超过k的串显然在同一组内。所以将同一组内的在原串中最早出现的后缀与最晚出现的后缀判断是否可行(这显然是最优的,理由略去),验证一次复杂度 O(n) O ( n )

4.只出现一次的子串的个数

建sam求路径数
好吧这里是sa。
考虑从i位置开始的子串,即考虑后缀i..n
那些出现多次的子串肯定在sa[rk[i]-1]与sa[rk[i]+1]中出现。
(因为排过序)
两个height取个max,然后用总长度减去max就是i开始只出现过一次的子串个数了。是不是比sam方便很多

你可能感兴趣的:(新内容,字符串)