后缀数组的SA-IS构造方法

SA-IS算法

之前的后缀数组都是用的倍增法来构造的,但是之前一场多校倍增的写法T了,就到网上学习了SA-IS算法,在此记录一下,长篇大论的写太耗时间了,这里就主要记录我个人的一些理解和要点以及我写全注释的代码,完整的学习博客可以参考以下几篇(在学习该算法前建议先熟悉基数排序):
线性求后缀数组
诱导排序与 SA-IS 算法(该篇格式有些问题)
后缀数组及SA-IS算法学习笔记

SA-IS算法的时间复杂度为 O ( n ) O(n) O(n),运行效率比DC3算法和倍增法都要高,常数较小且实现简单。

SA-IS 算法是基于诱导排序这种思想。基本想法就是将问题的规模缩小,通过解决更小的问题,获取足够信息,就可以快速的解决原始问题。从这里也可以看出,这一过程需要递归处理子问题。

该算法的基本框架如下:
1.将所有的后缀分为为S型后缀和L型后缀(将在后面介绍这两种后缀的定义)
2.倒序扫描s数组的所有后缀,得到后缀类型,即type数组
3.正序扫描type数组得到所有LMS字符的位置和顺序
4.对所有*型后缀进行诱导排序,处理出*型后缀的后缀数组
5.对每个LMS子串重新命名,得到新的数组s1
6.若s1中有重复的命名,递归计算s1
7.诱导排序前半计算sa1,后半利用sa1诱导计算sa

规定一些定义:
S [ x ] S[x] S[x] 表示 S S S 中下标为 x x x 的字符。下标从 0 0 0 开始,定义 # \# # 是字典序最小的字符,并且将其默认作为每个字符串的最后一个字符。 S [ a , b ] S[a, b] S[a,b] 表示 S S S 的一个子串,定义一个字符串 S S S 的前缀为 prefix ( S , i ) = S [ 0 , i ] \text{prefix}(S, i) = S[0, i] prefix(S,i)=S[0,i],其后缀为 suffix ( S , i ) = S [ i , ∣ S ∣ − 1 ] \text{suffix}(S, i) = S[i, |S| - 1] suffix(S,i)=S[i,S1]。定义函数 lcp ( A , B ) \text{lcp}(A, B) lcp(A,B) 表示 A A A B B B 的最长公共前缀的长度,即两者所有相同的前缀中,最长的一个的长度。

需要了解的性质:
定理 1 A < B A < B A<B 当且仅当 A [ lcp ( A , B ) ] < B [ lcp ( A , B ) ] A[\text{lcp}(A, B)] < B[\text{lcp}(A, B)] A[lcp(A,B)]<B[lcp(A,B)]

证明 这个结论显然成立,因为 prefix ( A , lcp ( A , B ) − 1 ) = prefix ( B , lcp ( A , B ) − 1 ) \text{prefix}(A, \text{lcp}(A, B) - 1) = \text{prefix}(B, \text{lcp}(A, B) - 1) prefix(A,lcp(A,B)1)=prefix(B,lcp(A,B)1)

引理 2  (后缀类型递推性质) 对于任意的 i ∈ [ 0 , ∣ S ∣ − 1 ] i \in [0, |S| - 1] i[0,S1]: 如果 t [ i ] = S-type t[i] = \text{S-type} t[i]=S-type,当且仅当下面任意一项成立:

S [ i ] < S [ i + 1 ] S[i] < S[i + 1] S[i]<S[i+1]
S [ i ] = S [ i + 1 ] S[i] = S[i + 1] S[i]=S[i+1] t [ i + 1 ] = S-type t[i + 1] = \text{S-type} t[i+1]=S-type
如果 t [ i ] = L-type t[i] = \text{L-type} t[i]=L-type,当且仅当下面任意一项成立:

S [ i ] > S [ i + 1 ] S[i] > S[i + 1] S[i]>S[i+1]
S [ i ] = S [ i + 1 ] S[i] = S[i + 1] S[i]=S[i+1] t [ i + 1 ] = L-type t[i + 1] = \text{L-type} t[i+1]=L-type

证明 这里证明 S S S 型的, L L L 型是类似的。对于第一种情况,显然是成立的。对于第二种情况,我们设 suffix ( S , i ) = a A , suffix ( S , i + 1 ) = a B \text{suffix}(S, i) = aA, \text{suffix}(S, i + 1) = aB suffix(S,i)=aA,suffix(S,i+1)=aB。由于第一个字符是相同的,因此我们需要比较 A A A B B B 的大小。因为它们是连续的后缀,所以 A = suffix ( S , i + 1 ) , B = suffix ( S , i + 2 ) A = \text{suffix}(S, i + 1), B = \text{suffix}(S, i + 2) A=suffix(S,i+1),B=suffix(S,i+2)。由于我们是从右往左推出 t t t,所以 A A A B B B 的关系实际上可以由 t [ i + 1 ] t[i + 1] t[i+1] 给出。故 t [ i ] = t [ i + 1 ] t[i] = t[i + 1] t[i]=t[i+1]
因此,我们可以在 Θ ( ∣ S ∣ ) \Theta(|S|) Θ(S) 的时间内,推出整个 t y p e type type 数组。

引理 3 (后缀类型指导排序) 对于两个后缀 A A A B B B,如果 A [ 0 ] = B [ 0 ] A[0] = B[0] A[0]=B[0] A A A S S S 型, B B B L L L 型,则 A > B A > B A>B

证明 设 A = a b X , B = a c Y A = abX, B = acY A=abX,B=acY,这里假设 a ≠ b a \neq b a=b a ≠ c a \neq c a=c。因为 A A A S S S 型,所以可知 a < b a < b a<b。同理, B B B L L L 型,可知 a > c a > c a>c。故 c < a < b c < a < b c<a<b,所以 A > B A > B A>B。如果 a = b a = b a=b a ≠ c a \neq c a=c,那么 b = a > c b = a > c b=a>c A > B A > B A>B,对于 a = c a = c a=c a ≠ b a \neq b a=b 同理。如果 a = b = c a = b = c a=b=c,则我们可以将 A A A B B B 的第一个字符去掉,用新的后缀来进行比较。根据引理 2.1,去掉第一个字符后的后缀类型不变。因此我们可以通过这样的操作直到变为第一种情况。

引理 4 # 是最短的 LMS 子串。

引理 5 对于任意的非 # 的 LMS 子串,其长度大于 2 2 2

证明 因为两个 LMS 字符中间必定有一个 L L L 型的后缀。

引理 6 (原串折半) 一个字符串中 LMS 子串的数量不超过 ⌈ ∣ S ∣ / 2 ⌉ \left\lceil{|S| / 2}\right\rceil S/2

证明 根据引理 2.4 可知。自己举几个实际的例子也很好理解。

引理 7 遍历一个字符串的所有 LMS 子串的时间复杂度级别为 O ( ∣ S ∣ ) O(|S|) O(S)

证明 根据引理5和引理6可知。

引理 8 (问题缩减) S 1 S_1 S1(重命名后的LMS子串数组) 中两个后缀的字典序关系,就是 S S S 中对应的 ∗ * 型后缀的字典序关系。

证明 我们可以将 S 1 S_1 S1 视为是将 ∗ * 后缀中不重合的部分进行切割并缩减。这样每一个 LMS 子串就可作为一个整体来进行比较。从而保持了这两者的一致性。

诱导排序的过程分为以下几步:
1.将 S A SA SA 数组初始化为每个元素都为 − 1 -1 1 的数组。
2.确定每个桶 S S S 型桶的起始位置。将 S A 1 SA_1 SA1 (重命名后的LMS子串数组的后缀数组)中的每一个 ∗ * 型后缀按照 S A 1 SA_1 SA1 中的顺序放入相应的桶内。
3.确定每个桶 L L L 型桶的起始位置。在 S A SA SA 数组中从左往右扫一遍。如果 S A [ i ] > 0 SA[i] > 0 SA[i]>0 t [ S A [ i ] − 1 ] = L-type t[SA[i] - 1] = \text{L-type} t[SA[i]1]=L-type,则将 S A [ i ] − 1 SA[i] - 1 SA[i]1 所代表的后缀放入对应的桶中。
4.重新确定每个桶 S S S 型桶的起始位置,因为所有的 ∗ * 型后缀要重新被排序。由于 S S S 型桶是逆序排放的,所以这次从右至左地扫描一遍 S A SA SA。如果 S A [ i ] > 0 SA[i] > 0 SA[i]>0 t [ S A [ i ] − 1 ] = S-type t[SA[i] - 1] = \text{S-type} t[SA[i]1]=S-type,则将 S A [ i ] − 1 SA[i] - 1 SA[i]1 所代表的后缀放入对应的桶中。

所谓的每个S型桶和L型桶的起始位置其实就是在SA数组中对每个字符划分出一个区域,该区域的长度就是该种字符在数组中出现的次数,每个S型从该区域的最后开始往前排,每个L型从该区域的最前开始往后排。

关于这个诱导排序的正确性证明和时间复杂度证明参见上面推荐的几篇博客。

SA-IS的模板性能可以通过这题来测试:后缀排序

下面是我的带注释的模板(在该题跑的效率不高,但是在牛客第一场A跑到了第二,且模板题的运行时间排名靠前的SA-IS算法较多):

#include <iostream>
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<vector>
#include<set>
#include<stack>
#include<cmath>
#include<string>
#define ll long long
#define ull unsigned long long
#define PI acos(-1.0)
#define pii pair<int,int>
#define fi first
#define se second
using namespace std;
const int N=1e5+100;
const int mod=1e9+7;
const int INF=1e9+7;
char str[N];
int n,m,sa[N],s[N<<1],tp[N<<1],lms[N],cnt[N],cur[N],rk[N],height[N];
//sa表示后缀数组,s表示转化为整数后的字符串数组,tp表示类型数组,lms表示LMS字符位置数组,cnt表示诱导排序中的桶数组,
//cur表示字符集中第i种字符的S型后缀和L型后缀的开始位置,s数组的额外空间用来存放重命名后的*型后缀,也用于递归,tp数组与s数组对应
void inducedsort(int n,int m,int n1,int *s,int *tp,int *v) //实际上还是以基数排序为主体进行实现
{
    fill_n(sa,n,-1); //初始化sa数组
    fill_n(cnt,m,0); //初始化桶
    for(int i=0;i<n;i++) cnt[s[i]]++; //将字符放入对应的桶
    for(int i=1;i<m;i++) cnt[i]+=cnt[i-1]; //前缀和求前i种字符的总数量
    for(int i=0;i<m;i++) cur[i]=cnt[i]-1; //S型后缀倒序存放,故字符集中第i种字符且为S型的开始位置为第i种字符的最后一位,*型后缀也是S型后缀
    for(int i=n1-1;i>=0;i--) sa[cur[s[v[i]]]--]=v[i]; //倒序存放*型后缀在原字符串中的位置
    for(int i=1;i<m;i++) cur[i]=cnt[i-1];   //L型后缀正序存放,故字符集中第i种字符且为L型的开始位置为第i种字符的第一位
    for(int i=0;i<n;i++) if(sa[i]>0&&tp[sa[i]-1]) sa[cur[s[sa[i]-1]]++]=sa[i]-1; //利用*型后缀的信息诱导正序存放L型后缀在原字符串中的位置
    for(int i=0;i<m;i++) cur[i]=cnt[i]-1; //S型后缀倒序存放,故字符集中第i种字符且为S型的开始位置为第i种字符的最后一位
    for(int i=n-1;i>=0;i--) if(sa[i]>0&&!tp[sa[i]-1]) sa[cur[s[sa[i]-1]]--]=sa[i]-1; //利用已有信息诱导逆序存放S型后缀(包括*型后缀)在原字符串中的位置
}
void sais(int n,int m,int *s,int *tp,int *lms) //SA-IS算法构造后缀数组
{
    int n1=tp[n-1]=0,ch=rk[0]=-1,*s1=s+n; //n1为LMS字符的数量,ch为重命名后的不同的LMS子串的数目减一,同时也作为该LMS子串的新名字,s1储存重命名后的LMS子串
    for(int i=n-2;i>=0;i--) tp[i]=s[i]==s[i+1]?tp[i+1]:s[i]>s[i+1]; //倒序判断后缀类型,1为L型后缀,0为S型后缀
    for(int i=1;i<n;i++) rk[i]=tp[i-1]&&!tp[i]?(lms[n1]=i,n1++):-1;//rk数组在这里并不是通常意义下的排名数组,这里只是为了少开一个数组利用了在排序过程中
    inducedsort(n,m,n1,s,tp,lms);//先粗略处理出*型后缀的后缀数组  //不会用到的rk数组,这里记录的是第i个字符是不是LMS字符,如果是,
    for(int i=0;i<n;i++)                                     //存下他是第n1(自加之前的n1)个,且将这个字符在原字符串的位置存到lms数组中;如果不是则置为-1。
    {
        int x=rk[sa[i]],y; //利用处理出的*型后缀的后缀数组对*型后缀重命名,通过rk数组可以得到第i个字符是不是LMS字符,是的话是第几个,方便在lms数组中找到
        if(x!=-1)
        {
            if(ch<1||(lms[x+1]-lms[x]!=lms[y+1]-lms[y])) ch++;  //如果一个LMS子串都还没有或者和前一个LMS子串长度不同则明显不相同,直接LMS子串数目加一
            else
            {
                for(int j=lms[x],k=lms[y];j<=lms[x+1];j++,k++)
                {
                    if((s[j]<<1|tp[j])!=(s[k]<<1|tp[k]))  //比较每一位字符值和类型是否都相同。字符值左移一位后和类型值或,类型值相同
                    {                                      //则最后一位相同,字符值相同则前面那些位相同,都相同最后结果才相同(花里胡哨)
                        ch++;
                        break;
                    }
                }
            }
            s1[y=x]=ch; //rk数组代表了该LMS字符是第几个,故能按照顺序将重命名的LMS子串放入s1中。同时该句也把当前的LMS字符位置赋给y方便下个循环的比较
        }
    }
    if(ch+1<n1) sais(n1,ch+1,s1,tp+n,lms+n1); //如果满足这个数量关系说明有重复的LMS子串,需要递归处理
    else
        for(int i=0;i<n1;i++)   //如果没有重复的就继续处理出新字符串数组s1的后缀数组
            sa[s1[i]]=i;    //这里sa数组只是用来储存每个新名字对应原字符串中的第几个LMS子串(也可以理解是第几个LMS字符)
    for(int i=0;i<n1;i++)
        s1[i]=lms[sa[i]];   //求每个新名字在原串中的位置,得到的就是sa1数组
    inducedsort(n,m,n1,s,tp,s1);    //利用sa1数组诱导排序求sa数组
}
void get_height()
{
    for(int i=0;i<n;i++) rk[sa[i]]=i;
    for(int i=0,k=height[0]=0;i<n-1;i++)
    {
        int j=sa[rk[i]-1];
        while(i+k<n&&j+k<n&&s[i+k]==s[j+k]) k++;
        if(height[rk[i]]=k) k--;
    }
}
int main()
{
    cin>>str;
    n=strlen(str);
    for(int i=0;i<n;i++) s[i]=str[i]-'a'+1; //将字符串转化为整数形式
    s[n++]=0; //一定要在最后加上一个比字符集中所有数都小的数
    m=27;
    sais(n,m,s,tp,lms);
    get_height();
    for(int i=1;i<n;i++) printf("%d%c",sa[i]+1,i==n-1?'\n':' ');
    for(int i=2;i<n;i++) printf("%d%c",height[i],i==n-1?'\n':' ');
    return 0;
}

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值