Loading

后缀数组——倍增算法

1 简介

后缀数组是后缀树的一个有力的替代品,倍增算法的复杂度是 \(O(n\log n)\) ,这个复杂度在大多数情况下不会成为瓶颈,sa-is 算法留着以后再学。

后缀数组所做的事情是把所有的后缀拿出来,然后按照字典序排序。

2 算法讲解

我们先把所有单个字符排序,实际上是给所有后缀的第一个字符排序。

然后我们在给所有后缀的前 \(2\) 个字符排序。

然后我们在给所有后缀的前 \(4\) 个字符排序。

直到所有排名两两不同。

注意每一次当我们给所有后缀的 \(2^i\) 排序的时候,我们实际可以利用我们上一次给 \(2^{i-1}\) 排序的结果。如下图:

这里需要实现一个双关键字排序,我们首选用基数排序加计数排序。

再用基数排序实现双关键字排序的时候注意要先排序第二关键字,然后再排序第一关键字。

3  实现分析

我们首先对一开始的数组进行预处理:

    for(int i=1;i<=n;i++) cnt[Rank[i]=s[i]]++;
    for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
    for(int i=n;i>=1;i--) sa[cnt[Rank[i]]--]=i;

一个简单的计数排序,我们就可以完成对单个字符的排序。这里不用关心值域,因为我们后面会再做调整。

排序代码:

inline bool Cmp(int x,int y,int w){
    return LastRank[x]==LastRank[y]&&LastRank[x+w]==LastRank[y+w];
}
inline void Sort(){
    for(int i=1;i<=n;i++) cnt[Rank[i]=s[i]]++;
    for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
    for(int i=n;i>=1;i--) sa[cnt[Rank[i]]--]=i;
    for(int w=1;;w<<=1,m=p){
        p=0;for(int i=n;i>n-w;i--) id[++p]=i;
        for(int i=1;i<=n;i++) if(sa[i]>w) id[++p]=sa[i]-w;
        memset(cnt,0,sizeof(cnt));
        for(int i=1;i<=n;i++) cnt[px[i]=Rank[id[i]]]++;
        for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
        for(int i=n;i>=1;i--) sa[cnt[px[i]]--]=id[i];
        memcpy(LastRank,Rank,sizeof(Rank));p=0;
        for(int i=1;i<=n;i++) Rank[sa[i]]=Cmp(sa[i],sa[i-1],w)?p:++p;
        if(p==n) break;
    }
}

我们考虑目前倍增是 \(w\),那么在 \(n-w\) 之后的一定都没有第二关键字,也就是说,第二关键字为空,所以如果我们首先按照第二关键字排序的话,一定是把这些放在前面。然后我们考虑枚举这个第二关键字,按照排名,把相对应的第一关键字放入 \(id\) 数组中,这样做的目的是使访问内存连续,优化常数。

然后我们对第一关键字进行排序,\(px\) 数组的目的也是为了优化常数,运用基数排序就可以,然后我们考虑更新 \(Rank\),这里需要注意,如果当前的和上一个的之前的排名第一关键字和第二关键字相等,这个时候就需要让这个后缀的排名相等。\(p\) 代表的是值域,可以用来缩小 \(m\) 的范围。如果 \(p\)\(n\) 相等,就没有两个相等的字符串了,这个时候再排序下去也没有意义,直接退出即可。

我们加上 \(Cmp\) 代码的目的也是为了使内存连续。

4 总代码

#include<bits/stdc++.h>
#define dd double
#define ld long double
#define ll long long
#define uint unsigned int
#define ull unsigned long long
#define N 1000100
#define M number
using namespace std;

const int INF=0x3f3f3f3f;

template<typename T> inline void read(T &x) {
    x=0; int f=1;
    char c=getchar();
    for(;!isdigit(c);c=getchar()) if(c == '-') f=-f;
    for(;isdigit(c);c=getchar()) x=x*10+c-'0';
    x*=f;
}

int n,m,p,Rank[N],sa[N],id[N],px[N],cnt[N],LastRank[N<<1];
char s[N];

inline bool Cmp(int x,int y,int w){
    return LastRank[x]==LastRank[y]&&LastRank[x+w]==LastRank[y+w];
}

inline void Init(){
    scanf("%s",s+1);n=strlen(s+1);m=1000;
}

inline void Sort(){
    for(int i=1;i<=n;i++) cnt[Rank[i]=s[i]]++;
    for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
    for(int i=n;i>=1;i--) sa[cnt[Rank[i]]--]=i;
    for(int w=1;;w<<=1,m=p){
        p=0;for(int i=n;i>n-w;i--) id[++p]=i;
        for(int i=1;i<=n;i++) if(sa[i]>w) id[++p]=sa[i]-w;
        memset(cnt,0,sizeof(cnt));
        for(int i=1;i<=n;i++) cnt[px[i]=Rank[id[i]]]++;
        for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
        for(int i=n;i>=1;i--) sa[cnt[px[i]]--]=id[i];
        memcpy(LastRank,Rank,sizeof(Rank));p=0;
        for(int i=1;i<=n;i++) Rank[sa[i]]=Cmp(sa[i],sa[i-1],w)?p:++p;
        if(p==n) break;
    }
}

inline void Print(){
    for(int i=1;i<=n;i++) printf("%d ",sa[i]);
}

int main(){
    Init();Sort();Print();return 0;
}

5 LCP 与 Height 数组

LCP 的定义是最长公共前缀,以下用 \(LCP(i,j)\) 表示编号为 \(i\) 的前缀和编号为 \(j\) 的前缀的 LCP,我们令 \(Height_i=LCP(sa_i,sa_{i-1})\),而 \(Height_1\) 可以视作 \(0\)。我们考虑如何求 \(Height\) 数组。

  • 引理:\(Height_{rk_i}\ge Height_{rk_{i-1}}-1\)

证明:如果 \(Height_{rk_{i-1}-1}\) 小于等于 \(1\),那么上面这个式子显然成立,这是因为 \(Height_i\ge 0\)

如果 \(Height_{rk_{i-1}-1}>1\),我们考虑设后缀 \(i-1\)\(aAD\),那么后缀 \(i\) 就是 \(AD\),其中 \(A\) 是一个长度为 \(Height_{rk_{i-1}}-1\) 的字符串,由于 \(Height\) 数组的定义,我们考虑后缀 \(sa_{rk_{i-1}-1}\) 这个字符串一定是 \(aAB\) 的形式,所以 \(sa_{rk_{i-1}-1}+1\) 这个后缀一定是 \(AB\) 的形式,所以这个后缀一定排在后缀 \(i\) 的前面,所以我们可以知道 \(Height_{rk_i-1}\) 一定包含字符串 \(A\),所以结论成立。

我们利用上面这个东西就可以 \(O(n)\) 的来求 \(Height\) 数组了。

代码:

inline void GetHeight(){
    for(int i=1,k=0;i<=n;i++){
        if(k) k--;
        while(s[i+k]==s[sa[rk[i-1]-1]+k]) k++;
        Height[rk[i]]=k;
    }
}

有一个结论是 \(LCP(sa_i,sa_j)=\min_{i+1\le k\le j}Height_k\),这个可以感性理解一下,如果 \(Height\) 一直大于某个值,那么这一段就一直有,反之,肯定是在一个后缀中改变。

还有一个结论是本质不同子串个数是 \(n(n-1)/2-\sum_{i=2}^ nHeight_i\)

考虑证明,其实比较简单,我们考虑首先计数,然后去重,所谓去重,就是去掉两个后缀的相同前缀,但我们其实不需要 \(n^2\) 枚举后缀,我们只需要把它们都加起来,考虑 \(LCP(sa_i,sa_j)\),设值为 \(x\),区间 \(i,j\) 中每一个都有这样的一个 \(x\),我们考虑这个东西已经被上面那个式子所包含。

posted @ 2021-04-23 19:13  hyl天梦  阅读(243)  评论(1编辑  收藏  举报