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 的出题人是真的报复社会。

咕咕咕

SA-IS学习笔记的更多相关文章

  1. Oracle学习笔记三 SQL命令

    SQL简介 SQL 支持下列类别的命令: 1.数据定义语言(DDL) 2.数据操纵语言(DML) 3.事务控制语言(TCL) 4.数据控制语言(DCL)  

  2. CSS3与页面布局学习笔记(八)——浏览器兼容性问题与前端性能优化方案

    一.浏览器兼容 1.1.概要 世界上没有任何一个浏览器是一样的,同样的代码在不一样的浏览器上运行就存在兼容性问题.不同浏览器其内核亦不尽相同,相同内核的版本不同,相同版本的内核浏览器品牌不一样,各种运 ...

  3. Entity Framework学习笔记——错误汇总

    之前的小项目做完了,到了总结经验和更新学习笔记的时间了.开始正题之前先啰嗦一下,对之前的学习目标进行一个调整:“根据代码生成表”与“生成数据库脚本和变更脚本”合并为“Code First模式日常使用篇 ...

  4. PHP操作MongoDB学习笔记

    <?php/*** PHP操作MongoDB学习笔记*///*************************//**   连接MongoDB数据库  **////*************** ...

  5. Solr学习笔记之3、Solr dataimport - 从SQLServer导入数据建立索引

    Solr学习笔记之3.Solr导入SQLServer数据建立索引 一.下载MSSQLServer的JDBC驱动 下载:Microsoft JDBC Driver 4.0 for SQL Server ...

  6. [转载]Log4net学习笔记

    Log4net 学习笔记: 主要是根据apache站点整理的: 原文链接:http://logging.apache.org/log4net/release/sdk/ http://logging.a ...

  7. OpenCV入门学习笔记

    OpenCV入门学习笔记 参照OpenCV中文论坛相关文档(http://www.opencv.org.cn/) 一.简介 OpenCV(Open Source Computer Vision),开源 ...

  8. Entity Framework学习笔记

    原文地址:http://www.cnblogs.com/frankofgdc/p/3600090.html Entity Framework学习笔记——错误汇总   之前的小项目做完了,到了总结经验和 ...

  9. OGG学习笔记02-单向复制配置实例

    OGG学习笔记02-单向复制配置实例 实验环境: 源端:192.168.1.30,Oracle 10.2.0.5 单实例 目标端:192.168.1.31,Oracle 10.2.0.5 单实例 1. ...

  10. OGG学习笔记04-OGG复制部署快速参考

    OGG学习笔记04-OGG复制部署快速参考 源端:Oracle 10.2.0.5 RAC + ASM 节点1 Public IP地址:192.168.1.27 目标端:Oracle 10.2.0.5 ...

随机推荐

  1. 实现一个简易vue

    vue主要的功能实现主要分为3部分: 数据劫持/数据代理:数据改变时通知相关函数进行更新操作 数据依赖收集:建立保存dom节点与数据的关联关系 模板与数据之间的绑定:接受到新数据时对dom节点进行更新 ...

  2. 调度器简介,以及Linux的调度策略(转)

    进程是操作系统虚拟出来的概念,用来组织计算机中的任务.但随着进程被赋予越来越多的任务,进程好像有了真实的生命,它从诞生就随着CPU时间执行,直到最终消失.不过,进程的生命都得到了操作系统内核的关照.就 ...

  3. 11Linux之软件包管理

    11Linux之软件包管理 目录 11Linux之软件包管理 11 软件包管理 11.1 软件包介绍 11.1.1 编程语言分类 11.1.2 三种安装包 11.2 rpm包管理 11.2.1 rpm ...

  4. bootstrap-datetimepicker 编辑回显

    官网上居然没给出解决方案....汗 stackoverflow给出了灵感: $("#dateOfManufacture").find("input").val( ...

  5. 3、Spring Boot日志

    1.日志框架 市面上的日志框架:JUL.JCL.Jboss-logging.logback.log4j.log4j2.slf4j.... 日志门面 (日志的抽象层) 日志实现 JCL(Jakarta ...

  6. 微信公众号平台Url Token EncodingAESKey 注意点

    最近公司让我开发微信公众号平台扫码登录,同步用户信息于PC端,所做的过程当中遇到了一些坑,做完了就总结一下需要注意的点,如若大家开发过程中遇到同样的问题,可以借鉴! 第一:配置域名 作用:配置域名为了 ...

  7. First day,beginning!

    beginning 在闲暇的时光记录当下的生活,一直是自己所期盼的: 由于种种原因(懒惰),一直未能开始,那么就从今天开始吧! 看下日期,从实习到现在一个月刚刚好: 公司很不错,师傅特别好,感觉自己是 ...

  8. [sql 注入] insert 报错注入与延时盲注

    insert注入的技巧在于如何在一个字段值内构造闭合. insert 报错注入 演示案例所用的表: MariaDB [mysql]> desc test; +--------+--------- ...

  9. MathType中怎么编辑韩文字符

    用MathType编辑公式,所涉及到符号与字母一般都是英文字母与数字,或者使用希腊字母,当然还有很多使用中文的情况.但是不仅如此,我们在使用MathType时,除了这些字符之外,还可以输入韩文或者日文 ...

  10. 庐山真面目之四微服务架构Consul和Ocelot简单版本实现

    庐山真面目之四微服务架构Consul和Ocelot简单版本实现 一.简介      在上一篇文章<庐山真面目之三微服务架构Consul简单版本实现>中,我们已经探讨了如何搭建基于Consu ...