SA-IS学习笔记

Q:SA-IS 是什么玩意?

A:一种 \(O(n)\) 求后缀数组的高科技。

Q:为什么会有 SA-IS 这种算法?

A:因为它是 \(O(n)\) 的,比倍增 \(O(n\log n)\) 快。

Q:SAM 不也是 \(O(n)\) 的吗?

A:SAM 空间是 \(O(n\sum)\) 的,随便就卡掉了。

Q:所以学习这个算法意义是什么。

A:出(bao)毒(fu)瘤(she)题(hui)。

由于SA-IS内涵卡常成分,如有部分奇怪的代码系正常情况,请放心食用。

SA-IS 全称 Suffix Array Induce Sort ,即诱导后缀排序。

为了方便,我们在字符串末尾加入一个特殊字符。我们认为特殊字符字典序最小。

\(rk_i\) 表示后缀 \(i\) 的排名,\(sa_i\) 表示排名为 \(i\) 的后缀。即有 \(sa_{rk_i}=i\)

定义:如果一段后缀 \(i\)\(rk_i>rk_{i+1}\) 那么 \(i\)\(\texttt L\) 型后缀。否则 \(i\)\(\texttt S\) 型后缀。

不妨设 \(op_i=[i \text{是 L 型后缀}]\)。特别的,特殊字符是S型后缀。

根据字符串的比较,其实我们可以很容易的得出一个后缀是 \(\texttt L\) 型还是 \(\texttt S\) 型。具体来说,如果 \(s_i=s_{i+1}\)\(op_i=op_{i+1}\) 否则 \(op_i=[s_i>s_{i+1}]\)

我们得出这个 \(op\) 后能推出什么呢?考虑一个字符串,它一定是由若干个 \(SS\cdots SLL\cdots LSS\cdots\) 构成的。

那么我们提取出它的所有 \(SS\cdots SLL\cdots LS\) 的子串,定义其叫做lms子串,然后将他们进行排序,将排序后的结果代替原串。

比如:

a a b a a b b a c b a b #
S S L S S L L S L L S L S
      *       *     *   *

那么lms子串就是 \([4,8],[8,11],[11,13]\)。注意这里区间都是闭区间,也就是相邻的lms子串有1的相交。

然后我们将这些子串排序,比如排序成 \(\{2,4,3,1\}\)

可以证明的是,如果两个后缀的位置同属于lms子串的分界点,那么这两个后缀的比较相当于在排序后的lms子串的后缀比较。

比如 \([8,13],[11,13]\) 的比较就可以转换成 \(431,31\) 的比较。

如果子串互不相等,相当于倍增后缀排序里字符两两不同一样,直接桶排序即可。

那么如果排序中存在lms子串相等的情况呢?递归处理lms子串的SA-IS结果即可。

然后我们只需要处理通过lms子串倒推所有串的关系。我们假设这部分和之前排序lms子串的部分复杂度都是 \(F(n)\)

那么就有 \(T(n)=T(\frac n 2)+F(n)\)

这部分代码还是比较可懂的:


void SA_IS(int n,int m,int *s,int *op,int *pos)
{
    int tot=0,cnt=0;int *S=s+n;//为了减少常数,这里直接取一段没有用过的地址而不是重新申请。
    op[n]=0;//为了方便,钦定最后一位是S型
    for(re int i=n-1;i;i--) op[i]=(s[i]!=s[i+1])?s[i]>s[i+1]:op[i+1];//O(n)求op
    rk[1]=0;
    for(re int i=2;i<=n;i++)
    if(op[i-1]==1 && op[i]==0) pos[++tot]=i,rk[i]=tot;//求出所有lms子串的端点
    else rk[i]=0;
    sa_sort(pos,n,m,s,op,tot);//排序lms子串。具体实现在后面
    int u=0,p=0;
    for(re int i=1;i<=n;i++)//去重,即unique
    if(rk[sa[i]])
    {
        u=rk[sa[i]];
        if(cnt<=1 || pos[u+1]-pos[u]!=pos[p+1]-pos[p]) ++cnt;//一个减小常数的优化:如果两个lms子串长度不一样,一定不同
        else
        {
            for(re int j=0;j<=pos[u+1]-pos[u];j++)//暴力判断。注意这里如果某个字符对应的lms后缀不同,也应当认为不同,因为如果首字母相同,L型后缀字典序一定大于S型。
            if(s[pos[u]+j]!=s[pos[p]+j] || op[pos[u]+j]!=op[pos[p]+j]){++cnt;break;}//因为lms子串总长度不超过 $O(n)$,所以暴力扫描复杂度是对的。
        }
        S[u]=cnt;//重新标号。
        p=u;
    }
    if(tot!=cnt) SA_IS(tot,cnt,S,op+n,pos+n);//cnt相当于不同数字个数,如果cnt==tot相当于所有数字两两相同,直接桶排序。为了方便,op和pos也直接取一段没有用过的地址。
    else for(re int i=1;i<=tot;i++) sa[S[i]]=i;
    for(re int i=1;i<=tot;i++) S[i]=pos[sa[i]];//得到真正的排名(之前的标号排的是lms子串,这里的排名是lms后缀)。
    sa_sort(S,n,m,s,op,tot);//利用lms子串得到真正的sa。
}

但是为了吊打倍增后缀数组,这里必须要有 \(F(n)=O(n)\)。这个乍一看有点困难。

但实际上是可行的,这个就是诱导排序。

为了方便,先考虑第二次,即通过lms子串的后缀数组倒推所有串的后缀数组。

我们把每个lms子串左侧部分看做是一条链,也就是说现在有若干个已知递增的序列,要归并成一个完整序列,且数字大小不超过字典序。这本质就是多路归并。

举个例子:比如字符串 \(\texttt{aabcabbacacacaa}\)

a a b c a b b a c a c a c a a #
S S S L S L L S L S L S L L L S
        *     *   *   *       *

#                *|
a#                |
aa#               |
aabcabbacacacaa#  |
abbacacacaa#     *|
abcabbacacacaa#   |
acaa#            *|
acacaa#          *|
acacacaa#        *|
bacacacaa#        |
bbacacacaa#       |
bcabbacacacaa#    |
caa#              |
cabbacacacaa#     |
cacaa#            |
cacacaa#          |

首先先随便钦定一个位置。这个位置必须满足符合首字母的区间以及lms子串的相对顺序。

比如 \(\texttt{abbacacacaa#}\) 子串可以放在 2,3,4,5,6 位置,但是不能放在1,因为1的首字母不是 \(\texttt{a}\)

\(\texttt{acaa#}\) 可以放在 3,4,5,6,7 位置,但是一定要放在 \(\texttt{abbacacacaa#}\) 之后。

这里采用一种比较方便的钦定方式:倒序放入区间末尾。

#                *| #
a#                |
aa#               |
aabcabbacacacaa#  |
abbacacacaa#     *|
abcabbacacacaa#   | abbacacacaa#
acaa#            *| acaa#
acacaa#          *| acacaa#
acacacaa#        *| acacacaa#
bacacacaa#        |
bbacacacaa#       |
bcabbacacacaa#    |
caa#              |
cabbacacacaa#     |
cacaa#            |
cacacaa#          |

然后我们先处理出所有 \(\texttt L\) 型后缀,我们考虑用已知的 sa 去推未知的。

具体来说,当前 sa 位置上有少数已经排好序的点,其余是0。

我们从左往右按顺序找到一个已经排好序且存在前缀的点 \(sa_i\),我们想要插入的后缀是 \(sa_i-1\)

首先这个操作前提是 \(sa_i-1\) 是一个 \(\texttt L\) 型后缀。然后可以发现,因为是“在开头加上一个字符”,当前处理的这个串是以这个串首字母为开头且未被放入的串中最大的。

#                *| #              <-#  <=i
a#                | a#             <-a  <=sa[i]-1
aa#               |                
aabcabbacacacaa#  |                
abbacacacaa#     *|                
abcabbacacacaa#   | abbacacacaa#   
acaa#            *| acaa#          
acacaa#          *| acacaa#        
acacacaa#        *| acacacaa#      
bacacacaa#        |                <-b
bbacacacaa#       |                
bcabbacacacaa#    |                
caa#              |                <-c
cabbacacacaa#     |                
cacaa#            |                
cacacaa#          |                

首先所有指针都移到最开头。

当前 \(i=1\) ,那么下一个就是 \(\texttt{a#}\)。直接赋值给 \(a\) 指针对应位置,然后 \(a\) 指针下移一位。

然后对于串 \(\texttt{a#}\),下一项是 \(\texttt{aa#}\),同样移动。

然后处理完所有 \(\texttt L\) 型后缀后变成:

#                *| #              
a#                | a#             
aa#               | aa#            
aabcabbacacacaa#  |                
abbacacacaa#     *|                
abcabbacacacaa#   | abbacacacaa#   <-a
acaa#            *| acaa#          
acacaa#          *| acacaa#        
acacacaa#        *| acacacaa#      
bacacacaa#        | bacacacaa#     
bbacacacaa#       |                <-b
bcabbacacacaa#    | bcabbacacacaa# 
caa#              | caa#           
cabbacacacaa#     | cabbacacacaa#  
cacaa#            | cacaa#         
cacacaa#          | cacacaa#       
                  |                <-c

然后我们恢复所有指针位置,对应到最末尾:

#                *| #              
a#                | a#             
aa#               | aa#            
aabcabbacacacaa#  |                
abbacacacaa#     *|                
abcabbacacacaa#   | abbacacacaa#   
acaa#            *| acaa#          
acacaa#          *| acacaa#        
acacacaa#        *| acacacaa#      <-a <=sa[i]-1
bacacacaa#        | bacacacaa#     
bbacacacaa#       |                
bcabbacacacaa#    | bcabbacacacaa# <-b
caa#              | caa#           
cabbacacacaa#     | cabbacacacaa#  
cacaa#            | cacaa#         
cacacaa#          | cacacaa#       <-c <=i

然后我们需要从后往前扫所有 \(\texttt S\) 型后缀的部分。同 \(\texttt L\) 型后缀,这样所有处理的后缀都是同字符开头且当前未添加的后缀中最大的。

我们发现一点:\(\texttt {cacacaa#}\) 的上一个前缀是 \(\texttt{acacacaa#}\)。其实直接赋值就可以了。

然后按顺序处理:

#                *| #              
a#                | a#             
aa#               | aa#            
aabcabbacacacaa#  |                
abbacacacaa#     *|                
abcabbacacacaa#   | abbacacacaa#   <-a <=sa[i]-1
acaa#            *| acaa#          
acacaa#          *| acacaa#        
acacacaa#        *| acacacaa#      
bacacacaa#        | bacacacaa#     
bbacacacaa#       | bbacacacaa#    <-b
bcabbacacacaa#    | bcabbacacacaa#     <=i
caa#              | caa#           
cabbacacacaa#     | cabbacacacaa#  
cacaa#            | cacaa#         
cacacaa#          | cacacaa#       <-c

这时我么发现当前位置出问题了,之前的后缀与当前要放的不符(左边一列就是后缀数组)。也就是说我们一开始的钦定出问题了。

这时候我们直接赋值即可。为什么?因为这个值一定会被下一个后缀所更新掉。

所以说,一开始的赋值只要相对位置没有问题,所在区间没有问题,最后都能得到结果。

最终数组就如左边一列所示。


然后对于lms子串的排序呢?这个好像是乱序的啊。

这里给出结论:我们可以直接按上述方式排序。

一遍排序 (\(\texttt L\) 型后缀)。

a a b c a b b a c a c a c a a #
S S S L S L L S L S L S L L L S
        *     *   *   *       *

#                *| #              <-#
a#                | a#             <-a
aa#               | aa#            
aabcabbacacacaa#  |                
abbacacacaa#     *|                
abcabbacacacaa#   | abbacacacaa#   
acaa#            *| acacacaa#      
acacaa#          *| acacaa#        
acacacaa#        *| acaa#          
bacacacaa#        | bacacacaa#     
bbacacacaa#       | bbacacacaa#    
bcabbacacacaa#    |                <-b
caa#              | caa#           
cabbacacacaa#     | cabbacacacaa#  
cacaa#            | cacacaa#       
cacacaa#          | cacaa#         
                  |                <-c

最终排序:

#                *| #                *
a#                | a#               
aa#               | aa#              
aabcabbacacacaa#  | aabcabbacacacaa# 
abbacacacaa#     *| abbacacacaa#     *
abcabbacacacaa#   | abcabbacacacaa#  
acaa#            *| acaa#            *
acacaa#          *| acacacaa#        *
acacacaa#        *| acacaa#          *
bacacacaa#        | bacacacaa#       
bbacacacaa#       | bbacacacaa#      
bcabbacacacaa#    | bcabbacacacaa#   
caa#              | caa#             
cabbacacacaa#     | cabbacacacaa#    
cacaa#            | cacacaa#         
cacacaa#          | cacaa#           

上面右排带 \(\texttt *\) 的就是排序后所有lms子串的对应位置。

但是8和10是不是错了啊!

注意我们排的并不是后缀,而是一个子串。而 \([8,10]\)\([10,12]\) 是本质相同的,所以怎么排都不算错。

对于正确性,一个感性的理解就是:如果一个串 \(a>b\)\(a\) 排在 \(b\) 前,那么 \(a\) 通过一系列 \(\texttt L\)\(\texttt S\) 后会将 \(b\) 排到自己前面。

然后回到上例,这里 \([8,10]\)\([10,12]\) 本质相同,所以最后去重时发现不是两两不同的,递归处理。

那么递归处理的串就是 \(\texttt{24431}\)

处理结束后返回的是 \(\texttt{25431}\)

然后根据上面的过程诱导排序即可。

总复杂度 \(O(n)\)

完整代码(UOJ#34):

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define N 1000010
#define re register
using namespace std;
char str[N];
int sa[N],rk[N],h[N],s[N<<1],op[N<<1],pos[N<<1],c1[N],c[N];
#define L(x) sa[c[s[x]]--]=x
#define R(x) sa[c[s[x]]++]=x
inline void sa_sort(int *S,int n,int m,int *s,int *op,int tn)
{
    for(re int i=1;i<=n;i++) sa[i]=0;
    for(re int i=1;i<=m;i++) c1[i]=0;
    for(re int i=1;i<=n;i++) c1[s[i]]++;
    for(re int i=2;i<=m;i++) c1[i]+=c1[i-1];
    for(re int i=1;i<=m;i++) c[i]=c1[i];
    for(re int i=tn;i;i--) L(S[i]);
    for(re int i=1;i<=m+1;i++) c[i]=c1[i-1]+1;
    for(re int i=1;i<=n;i++)
    if(sa[i]>1 && op[sa[i]-1]) R(sa[i]-1);
    for(re int i=1;i<=m;i++) c[i]=c1[i];
    for(re int i=n;i;i--)
    if(sa[i]>1 && !op[sa[i]-1]) L(sa[i]-1);
}
void SA_IS(int n,int m,int *s,int *op,int *pos)
{
    int tot=0,cnt=0;int *S=s+n;
    op[n]=0;
    for(re int i=n-1;i;i--) op[i]=(s[i]!=s[i+1])?s[i]>s[i+1]:op[i+1];
    rk[1]=0;
    for(re int i=2;i<=n;i++)
    if(op[i-1]==1 && op[i]==0) pos[++tot]=i,rk[i]=tot;
    else rk[i]=0;
    sa_sort(pos,n,m,s,op,tot);
    int u=0,p=0;
    for(re int i=1;i<=n;i++)
    if(rk[sa[i]])
    {
        u=rk[sa[i]];
        if(cnt<=1 || pos[u+1]-pos[u]!=pos[p+1]-pos[p]) ++cnt;
        else
        {
            for(re int j=0;j<=pos[u+1]-pos[u];j++)
            if(s[pos[u]+j]!=s[pos[p]+j] || op[pos[u]+j]!=op[pos[p]+j]){++cnt;break;}
        }
        S[u]=cnt;
        p=u;
    }
    if(tot!=cnt) SA_IS(tot,cnt,S,op+n,pos+n);
    else for(re int i=1;i<=tot;i++) sa[S[i]]=i;
    for(re int i=1;i<=tot;i++) S[i]=pos[sa[i]];
    sa_sort(S,n,m,s,op,tot);
}
int ht[N];
void get_ht(int n)
{
	for(re int i=1;i<=n;i++) rk[sa[i]=sa[i+1]]=i;
	for(re int i=1,p=0;i<=n;ht[rk[i]]=p,i++)
	if(rk[i]!=1) for(p=p-!!p;sa[rk[i]-1]+p<=n && i+p<=n && s[i+p]==s[sa[rk[i]-1]+p];p++);
}
namespace IO
{
    char obuf[(1<<21)+1],st[11],*oS=obuf,*oT=obuf+(1<<21);
    void Flush(){fwrite(obuf,1,oS-obuf,stdout),oS=obuf;}
    void Put(char x){*oS++=x;if(oS==oT)Flush();}
    void write(int x){int top=0;if(!x)Put('0');while(x)st[++top]=(x%10)+48,x/=10;while(top)Put(st[top--]);Put(' ');}
}using namespace IO;
int main()
{
    int n;
    fread(str+1,1,100000,stdin),n=strlen(str+1);
    while(!isalpha(str[n])) --n;
    for(int i=1;i<=n;i++) s[i]=str[i]-'a'+2;
    s[++n]=1;
    SA_IS(n--,28,s,op,pos);
    get_ht(n);
    for(int i=1;i<=n;i++) write(sa[i]);Put('\n');
    for(int i=2;i<=n;i++) write(ht[i]);Flush();
    return 0;
}

后记:

Q:所以 SA-IS 真的有用吗?好像没有什么题目要用到啊。

A:有还是有的。不过卡 SAM 的出题人是真的报复社会。

咕咕咕

posted @ 2020-10-23 22:18  Flying2018  阅读(843)  评论(1编辑  收藏  举报