后缀数组

后缀数组是一个比较强大的处理字符串的算法,是有关字符串的基础算法,所以必须掌握。
学会后缀自动机(SAM)就不用学后缀数组(SA)了?,虽然SAM看起来更为强大和全面,但是有些SAM解决不了的问题能被SA解决,只掌握SAM是远远不够的。

顾名思义,后缀数组就是记录所有后缀的数组,同时,它也是有序的。后缀数组 SA 可以帮助我们解决单字符串问题、两个字符串的问题和多个字符串的问题等

比如说字符串 banana¥我们暂且把¥ 认为是一个字符(表示字符串结尾)。
我们记suffix(i)表示从原字符串第i个字符开始到字符串结尾的后缀。我们把它所有的后缀拿出来按字典序排序:
这里写图片描述

把排好序的数组记作sa

那么此时sa数组就是:7 6 4 2 1 5 3

把s的每个后缀按照字典序排序,

后缀数组sa[i]就表示排名为i的后缀的起始位置的下标

而它的映射数组rk[i]就表示起始位置的下标为i的后缀的排名

简单来说,sa表示排名为i的是啥,rk表示第i个的排名是啥

一定要记牢这些数组的意思,后面看代码的时候如果记不牢的话就绝对看不懂。
也就是说如果sa[i]=j那么 rank[j]=i。

sa与rank是互逆运算。看图

这就是Rank和SA
我们现在令 height[i] 是 suffix(sa[i-1]) 和 suffix(sa[i]) 的最长公共前缀长度,即排名相邻的两个后缀的最长公共前缀长度。

比如height[4]就是anana和ana和ana的最长公共前缀,也就是ana,长度为3。

这个height数组有一个神奇的性质:若rank[j]<rank[k],则后缀Sj..n和 Sk..n的最长公共前缀为min(height[rank[j]+1],height[rank[j]+2]...height[rank[k]])。这个性质是显然的,因为我们已经后缀按字典序排列。

同时,我们还有一个结论:height[rank[i]]≥height[rank[i−1]]−1。

倍增算法


倍增算法的主要思想 :对于一个后缀Suffix[i],如果想直接得到Rank比较困难,但是我们可以对每个字符开始的长度为2k的字符串求出排名,k从0开始每次递增1(每递增1就成为一轮),当2k大于Len时,所得到的序列就是Rank,而SA也就知道了。O(logn)枚举k
这样做有什么好处呢?
设每一轮得到的序列为rank(注意r是小写,最终后缀排名Rank是大写)。有一个很美妙的性质就出现了!第k轮的rank可由第k - 1轮的rank快速得来!
为什么呢?为了方便描述,设SubStr(i, len)为从第i个字符开始,长度为len的字符串我们可以把第k轮SubStr(i, 2k2k)看成是一个由SubStr(i, 2k−1)和SubStr(i + 2k−1 2k−1)拼起来的东西。类似rmq算法,这两个长度而2k−1的字符串是上一轮遇到过的!当然上一轮的rank也知道!那么吧每个这一轮的字符串都转化为这种形式,并且大家都知道字符串的比较是从左往右,左边和右边的大小我们可以用上一轮的rank表示,那么……这不就是一些两位数(也可以视为第一关键字和第二关键字)比较大小吗!再把这些两位数重新排名就是这一轮的rank。
我们用下面这张经典的图理解一下:
就像一个两位数的比较

还有一个细节就是怎么把这些两位数排序?这种位数少的数进行排序毫无疑问的要用一个复杂度为长度*排序数的个数的优美算法——基数排序(对于两位数的数复杂度就是O(Len)的)。
基数排序原理 : 把数字依次按照由低位到高位依次排序,排序时只看当前位。对于每一位排序时,因为上一位已经是有序的,所以这一位相等或符合大小条件时就不用交换位置,如果不符合大小条件就交换,实现可以用”桶”来做。


构造最长公共前缀——Height


同样先是定义一些变量
Heigth[i] : 表示Suffix[SA[i]]和Suffix[SA[i - 1]]的最长公共前缀,也就是排名相邻的两个后缀的最长公共前缀
H[i] : 等于Height[Rank[i]],也就是后缀Suffix[i]和它前一名的后缀的最长公共前缀
而两个排名不相邻的最长公共前缀定义为排名在它们之间的Height的最小值。
跟上面一样,先形像的理解一下:
这就是Height

高效地得到Height数组
如果一个一个数按SA中的顺序比较的话复杂度是O(N^2)级别的,想要快速的得到Height就需要用到一个关于H数组的性质。
H[i] ≥ H[i - 1] - 1!
如果上面这个性质是对的,那我们可以按照H[1]、H[2]……H[Len]的顺序进行计算,那么复杂度就降为O(N)了!
让我们尝试一下证明这个性质 : 设Suffix[k]是排在Suffix[i - 1]前一名的后缀,则它们的最长公共前缀是H[i - 1]。都去掉第一个字符,就变成Suffix[k + 1]和Suffix[i]。如果H[i - 1] = 0或1,那么H[i] ≥ 0显然成立。否则,H[i] ≥ H[i - 1] - 1(去掉了原来的第一个,其他前缀一样相等),所以Suffix[i]和在它前一名的后缀的最长公共前缀至少是H[i - 1] - 1。
仔细想想还是比较好理解的。H求出来,那Height就相应的求出来了,这样结合SA,Rank和Height我们就可以做很多关于字符串的题了

Code:

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<vector>
#include<stdlib.h>
#include<queue>
#include<map>
#include<iomanip>
#include<math.h>
using namespace std;
typedef long long ll;
typedef double ld;
const int maxn = 1000010;
int n;
char s[maxn];
int sa[maxn], rank[maxn], height[maxn];
int p[maxn], tmp[maxn], cnt[maxn];
bool equ(int x,int y,int l)
{
    return rank[x] == rank[y] && rank[x + l] == rank[y + l];
}
void doubling()
{
    for(int i = n; i; --i)
        s[i] = s[i - 1];
    for(int i = 1; i <= n; ++i)
    {
        rank[i] = s[i];
        sa[i] = i;
    }
    for(int l = 0, pos = 0, sig = 255; pos < n; sig = pos)
    {
        pos = 0;
        for(int i = n - l + 1; i <= n; ++i)
            p[++pos] = i;
        for(int i = 1; i <= n; ++i)
            if(sa[i] > l)
                p[ ++pos ] = sa[i] - l;
        memset( cnt, 0,sizeof(int) * (sig + 1));
        for(int i = 1; i <= n; ++i)
            ++cnt[rank[i]];
        for(int i = 1; i <= sig; ++i)
            cnt[i] += cnt[i - 1];
        for(int i = n; i; --i)
            sa[cnt[rank[p[i]]] -- ] = p[i];
        pos = 0;
        for(int i = 1; i <= n; ++i)
            tmp[sa[i]] = equ(sa[i], sa[i - 1], l)?pos:++pos;
        for(int i = 1; i <= n; ++i)
            rank[i] = tmp[i];
        l = !l? 1 : l << 1;
    }
    return;
}
int main()
{
    scanf("%s",s);
    n = strlen(s);
    doubling();
    for(int i = 1; i <= n; ++i)
        printf("%d ",sa[i]);
    return 0;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值