【字符串数据结构后缀系列Part1】后缀数组学习笔记

时间:2022-04-18 19:03:27

AC自动机好厉害啊www所以我要学后缀自动机和后缀数组啦(有什么关系吗魂淡(╯‵□′)╯︵┻━┻)
没关系这并不妨碍什么= =

——————————————–线割分是我>w<————————————————–
根据方法不同,字符串匹配算法/数据结构分成了前缀和后缀两大类.前缀以AC自动机,KMP和Trie最为出名,后缀有代表性的就是后缀自动机/后缀数组/后缀树.
我就是学后缀你来咬我啊www

先来几个定义~(≧▽≦)/~
一.通常我们用 表示一个字符集(每个元素都是一个字符),其中每个字符都是互异的,并且其中的字符两两之间可以进行比较大小.
二.字符串是由 中若干个字符按照一定顺序排列形成的数组.长度为n的字符串s,其长度记作len(s),串S中的第i个字符记作s[i].
三.字符串s中第i位到第j位的连续字符构成的字符串,称为s的一个子串.注意,空串也算s的一个子串.
四.选取s的第i位,则从第一位到第i位的子串称为s的一个前缀,相应的第i位到第n位构成的子串称为s的一个后缀.
五.字典序的比较原则,是从两个串s1,s2的第一位开始,逐个向后比较,若存在一个字符 s1[i]>s2[i] ,则串s1字典序大于串s2, s1[i]<s2[i] s1<s2 ;如果直到扫到了两串中某串的最后一位仍然未得到比较结果,则根据长度比较. len(s1)<len(s2) ,则 s1<s2 ;否则balabala= =
显然对一个字符串的每一个后缀,他们的字典序都不可能相等,这也是使用后缀数据结构的重要条件.

基础知识说完了,该开始说后缀数组啦=- =
后缀数组:我们先把字符串的所有后缀按照字典序从小到大排序,然后建立一个叫sa的数组,sa[i]表示排序后第i个字符串表示的后缀是从原串s的第几位开始的.
我们用Suffix[i]表示第i位起的后缀,则很显然

Suffix[sa[i]]<Saffix[sa[i+1]]

后缀数组从不会独自出现,和它同时使用的是名次数组.
名次数组:定义名次数组rank,则rank[i]表示原串中第i位开始记的后缀在所有后缀里字典序从小到大排多少
故后缀数组sa和rank其实是一对互逆运算.
怎么互逆?有SA[i]=j, 则 Rank[j]=i
因此,这两个数组我们只要求出了任意一个,就可以 O(n) 的求出另一个.

那么问题来了.怎样构造后缀数组?
显然最裸的思路是搞出所有后缀然后让他们排序再搞去吧= =
但是显然不行QAQ.
比较两个字符串是 O(n) 的,再算上排序就成了 O(n2 logn)
这么慢的话要你何用(╯‵□′)╯︵┻━┻
于是人们提出了两种方法:DA(倍增)和DC3算法.

倍增建后缀数组:
以字符串s的每一位开始,取从某位开始长度为 2k 的子串,对子串进行排序并求rank.可以看到,当子串足够长之后,这个子串就变成了整个字串的后缀,即可求出整个rank数组.当然,在倍增的时候我们并不需要重新取出所有串,因为之前的rank的贡献仍然可以保留.因此只需要利用新倍增出的 2k1 的子串的rank(两个 2k1rank ),就可以得到 2k 串的rank.为了加速,我们在排序时使用基数排序(当然如果待排序子串非常非常大就只能快排了),因此总复杂度就是 O(nlogn) .(快排要多一个log)

接下来让我们来体会一下酸爽的代码.相信很多人都见到过构建后缀数组的代码,那十多个乱七八糟的for实在是让人蛋疼无比.

膜罗穗骞论文.论文中代码写法在很多方面都比刘汝佳训练指南写法要优.

#include<bits/stdc++.h>
using namespace std;
#define MAXN 100010
char s[MAXN];
int A[MAXN];
//为了方便基数排序,我们把s中的字符转换成数字.s和A的下标使用统一0-n-1 
int sa[MAXN],rank[MAXN];//最后结果的rank和sa
int Count[MAXN];//基数排序计数器 
int l[MAXN],r[MAXN],tmp[MAXN];//基数排序共有两个关键字,r为第二关键字基数排序结果,l值即临时rank值. 
int n,maxn;//计数上界 
bool comp(int *A,int a,int b,int len)//字符串比较 
{
    return A[a]==A[b]&&A[a+len]==A[b+len];
}
int main()
{
    /*省略读入等奇怪的过程*/
    int i,j,k,*x=l,*y=r;//后面会整体交换l,r为了方便使用指针 
    for (i=0;i<maxn;i++)    Count[i]=0;
    for (i=0;i<n;i++)   Count[x[i]=A[i]]++;//第一次基数排序 
    for (i=1;i<maxn;i++)    Count[i]+=Count[i-1];
    for (i=n-1;i>=0;i--)    sa[--Count[x[i]]]=i;//初始的sa 
    for (k=1,i=1;i<n;k<<=1,maxn=i)
    {
        for (i=0,j=n-i;j<n;j++) y[i++]=i;//第二次基数排序
        for (j=0;j<n;j++)   if (sa[j]>=k)   y[i++]=sa[j]-k;
        for (j=0;j<n;j++)   tmp[j]=x[y[j]];
        for (j=0;j<maxn;j++)    Count[j]=0;
        for (j=0;j<n;j++)   Count[tmp[j]]++;
        for (j=1;j<maxn;j++)    Count[j]+=Count[j-1];
        for (j=n-1;j>=0;j--)    sa[--Count[tmp[j]]]=y[j];//更新sa 
        for (swap(x,y),i=1,x[sa[0]]=0,j=1;j<n;j++)  x[sa[j]]=comp(y,sa[j-1],sa[j],k)?i-1:i++;//更新rank
        //在更新过程中可能有两字符串rank值相同,此时比较两字符串是否完全相同来区分rank值
        //由于y数组在被用来更新sa后已经无用(下一次会重新求),节省空间使用y保存rank 
    }
}

SA的应用我准备在SAM之后单独写blog=- =(其实是懒得写在这里+骗分)