后缀数组 学习笔记

发布时间 2023-10-30 10:21:06作者: Ehundategh

后缀数组 学习笔记

定义

我们定义后缀数组 \(Sa\) 中的元素 \(Sa_i\) 为,字典序排名为 \(i\) 的后缀所在的位置。我们定义排名数组 \(Rank\) 中的元素 \(Rank_i\) 为,在位置 \(i\) 的后缀的排名。

求解后缀数组

首先 \(O(n^2logn)\) 的解法很显然,直接排序。

那么有一个利用倍增的思想求解后缀数组的 \(O(nlog^2n)\)的解法 ,首先,我们优先对单个字符排序,发现出现重复的话,我们倍增扩大长度,再次进行比较,直到每个后缀的排名各不相同。

其实还有 \(O(nlogn)\)\(O(n)\) 的解法,详见 后缀数组简介 - OI Wiki

Code

/*
 * @Author: Ehundategh
 * @Date: 2023-10-22 20:15:56
 * @FilePath: \Code\Model\String\SA.cpp
 * @Description: You Steal,I kill
 */
#include <cstdio>
#include <cstring>
#include <algorithm>
#define MAXN 2000010
using namespace std;
int Suffix_Array[MAXN],Rank[MAXN],n,k,Height[MAXN];
int Temp[MAXN];
char In[MAXN];
bool cmpsa(int i,int j){
    if(Rank[i]!=Rank[j]){
        return Rank[i]<Rank[j];
    }
    else{
       int Secondi=i+k<=n?Rank[i+k]:-1;
       int Secondj=j+k<=n?Rank[j+k]:-1;
       return Secondi<Secondj;
    }
}
void Get_SA(){
    for(int i=1;i<=n;i++){
        Suffix_Array[i]=i; Rank[i]=In[i];
    }
    for(k=1;k<=n;k*=2){
        sort(Suffix_Array+1,Suffix_Array+n+1,cmpsa);
        Temp[Suffix_Array[1]]=0;
        for(int i=1;i<=n;i++){
            Temp[Suffix_Array[i+1]]=Temp[Suffix_Array[i]]+(cmpsa(Suffix_Array[i],Suffix_Array[i+1])?1:0);
        }
        for(int i=1;i<=n;i++) Rank[i]=Temp[i];
    }
    return;
}
int main(){
    scanf("%s",In+1);
    n=strlen(In+1);
    Get_SA();
    for(int i=1;i<=n;i++) printf("%d ",Suffix_Array[i]);
}

这样就可以求出后缀数组了。

例题:

[[JSOI 2007]字符加密]([P4051 JSOI2007] 字符加密 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn))

思路
首先我们先把字符串复制一遍,那么将全新的字符串的后缀排序,从后缀排名从小到大遍历,如果出现后缀位置在原串中,就输出当前位置前一个位置的字符

高度数组

高度数组,后缀数组的神。

分为以下三个部分讲解:

  • 定义
  • 求解
  • 例题

定义

首先我们先定义 \(LCP(S1,S2)\)(longest Common Prefix) 即最长公共前缀,为:一个最大的 \(x\) 满足 $ S1_i=S2_i (\forall1\le i\le x)$。而我们下面提到的 \(LCP(a,b)\),都是表示以 \((a,b)\) 两个位置为起点的后缀的最长公共前缀。

我们定义:\(Height_i=LCP(Sa_i,Sa_{i-1})\),特别地, \(Height_1=0\)

举个例子,方便理解高度数组。

我们来看以下的字符串:\(\texttt{abcbc}\),对于这一个字符串,我们尝试求解其高度数组。

首先有 \(Height_1=0\)

然后我们来求解 \(Height_2\),将 \(Sa_2\)\(Sa_1\) 作比较求最长公共前缀,也就是求解 \(\texttt{abcbc}\)\(\texttt{bc}\) 的最长公共前缀,也就是 \(0\)

然后对于 \(Height_3\),将 \(Sa_3\)\(Sa_2\) 作比较求最长公共前缀,那么比较 \(\texttt{bc}\)\(\texttt{bcbc}\) 即可,答案为 \(2\)

对于 \(Height_4\)\(Height_5\) 同理可得答案为,\(0,1\)

求解

求解有一个神奇的引理 \(Height_{Rank_i} \ge Height_{Rank_{i-1}}-1\)

关于这个引理的证明。详见2009国家队论文。

那么我们就可以通过这个引理,来实现 \(O(n)\) 时间复杂度的高度数组处理。

每次尝试与排名上一个的后缀比较,拓展得到当前的 \(Height_i\) 即可,而拓展也不用从 \(0\) 开始,从上次的 \(Height_i-1\) 开始即可,这样高度数组最多变化 \(3n\) 次,所以时间复杂度是 \(O(n)\)

代码

void Get_Height(){
    for(int i=1,k=0;i<=n;i++){
        if(Rank[i]==0)continue;
        if(k>0) --k;
        while(In[i+k]==In[Suffix_Array[Rank[i]-1]+k])++k;
        Height[Rank[i]]=k;
    }
}

例题

ADASTRNG - Ada and Substring

思路
高度数组模板题,对于每一个字符开头的后缀只需要每次统计总共的子串个数,然后再减去两两后缀的最长公共前缀去重即可。

UVA Editor

思路
对于每一组询问,求Height的max即可。(话说这道题有一个加强版,要求出现k次,但是其实是一个做法,只需要求相邻k-1个的Height的max即可)