后缀数组SA学习笔记
什么是后缀数组
后缀数组\(sa[i]\)表示字符串中字典序排名为\(i\)的后缀位置
\(rk[i]\)表示字符串中第\(i\)个后缀的字典序排名
举个例子:
ababa
a b a b a
rk:3 5 2 4 1
sa: 5(a) 3(aba) 1(ababa) 4(ba) 2(baba)
那么就有\(sa[rk[i]]=rk[sa[i]]=i\)
后缀数组的求法
二周目
倍增法
看一会儿还是比较好记的
但没有理解每句话是在干什么的话以后再写就会没有思路
因此这里简述一下基本过程和一些关键细节
之后使用后缀\(i\)表示位置为\(i\)的后缀,第\(i\)个后缀表示排名为\(i\)的后缀,特此说明
倍增法的基本过程:
首先将所有后缀的第一个字符拿出来做一次排序,并记录其排名,字母相同的排名相同
第二步,将所有后缀的第一个字母和第二个字母拿出来做排序;
我们知道后缀\(i\)的第二个字母的排名就是后缀\(i+1\)的排名,后缀\(n\)没有第二个字母,因此其排名为\(0\)
此时对于\((rk[i],rk[i+1])\)的二元组进行第二次排序,并重新记录其排名
之后求出包括前四个字母\((rk[i],rk[i+2])\)的排名,包括前\(2p\)个字母\((rk[i],rk[i+p])\)的排名...直到\(p\ge n\)为止。
如果直接按照上面的方法模拟,复杂度将是\(O(nlog^2n)\)的
关键细节:基数排序
其实直接模拟用快排求后缀数组思想比较简单
但是基数排序求后缀数组代码简单并且复杂度为\(O(nlogn)\)较为优秀
所以现在一般使用的算法都是基数排序。
我们的基数排序到底是个什么东西呢?
假设我们现在要对一些二元组\((a,b)\)进行排序
设\(x\)表示每个元素对应的a离散之后的权值
\(y\)表示将每个元素按照b排序后的元素编号,\(y\)数组为一个\(1-n\)的排列
\(z\)表示将每个元素按照(a,b)排序后的编号,
注意由于\(y\)数组的存在,求出的\(z\)的排名一定是互不相同的
\(t\)表示桶数组
则代码如下:
int n;
int x[N],y[N],t[N],z[N];
inline void Rsort(){
for(int i=1;i<=n;i++)t[x[i]]++;
for(int i=1;i<=n;i++)t[i]+=t[i-1];
for(int i=n;i;i--)z[t[x[y[i]]]--]=y[i];
}
第一句话就是把所有元素的第一维全部丢到桶里去
第二句话就是把所有的桶前缀和,前缀和之后\(t[i]\)表示的是第一维(a)权值\(\le i\)的元素个数
前两句话都容易理解。
但是第三句话是个什么东西?那么多的数组调用是怎么回事?
别急,我们慢慢分析。
求出了\(t[i]\)之后,我们怎么求出元素现在的排名啊?
因为我们的\(t[i]\)表示的是第一维(a)权值\(\le i\)的元素个数
那么如果第一维(a)权值为\(i\)的元素的个数为\(c_i\)个,
那么权值为\(i\)的元素的排名区间就是\([t[i-1]+1=t[i]-c[i],t[i]]\)
而我们的只要对于每一个\(i\)令\(t[x[i]]--\)就可以保证结果的第一维(a)一定是符合要求的
像这样
for(int i=1;i<=n;i--)z[t[x[i]]]=i,t[x[i]]--;
即
for(int i=1;i<=n;i--)z[t[x[i]]]=i,t[x[i]]--;
但这样无法满足第二维符合要求,怎么办呢?
我们发现上面的代码中\(i\)的枚举顺序是可以改变而不影响第一维(a)排序后符合要求这个条件的
于是我们现在就是要找到一种枚举\(i\)的顺序,使得第二维(b)排序后也符合要求
我们知道在权值为\(i\)的所有元素中第一个使\(t[i]\)自减的元素得到的排序位置是最大的
所以我们按照倒序枚举\(i\)第二维的编号排名\(y[i]\),
for(int i=n;i;i--)//change i to y[i]
这样第一个权值为\(i\)的所有元素中第一个使\(t[i]\)自减的元素的第二维肯定是最大的。
所以这样排序就符合要求啦
for(int i=n;i;i--)z[t[x[y[i]]]--]=y[i];
主体部分
这里都是一些细节类的东西,我就直接放在注释里啦
inline void getsa(){
for(int i=1;i<=n;i++)y[i]=i,t[x[i]=a[i]]++;Rsort();
for(int k=1,p;k<=n;k<<=1){
p=0;
for(int i=n-k+1;i<=n;i++)y[++p]=i;
//最后k位是没有权值的,并且已经排好序,因此第二维的值为0,需要放在最前面
for(int i=1;i<=n;i++)if(sa[i]>k)y[++p]=sa[i]-k;
//这里按照后缀的排名进行正序枚举,枚举第二维可能出现的后缀位置,如果$sa[i]>k$证明这个后缀会后缀$sa[i]-k$作为二元组,它在第二维中的排名肯定是所有未枚举中最靠前的
Rsort();
swap(x,y);
//此时y变成了x,即原序号;
//我们应当记得我们是要对(x[i],x[i+k])的二元组进行排序
x[sa[1]]=p=1;
for(int i=2,p0,p1;i<=n;i++){
p0=sa[i-1];p1=sa[i];
x[p1]=(y[p0]==)?p:++p;
}
//仍然正序枚举后缀的排名,离散化
//枚举sa[i]其实就是枚举基数排序后的第i个元素
}
}
完整代码
int n,m,sa[N],x[N],y[N],t[N];
char s[N];
il bool cmp(int i,int j,int k){return y[i]==y[j]&&y[i+k]==y[j+k];}
il void getsa(){
m=200;for(RG int i=1;i<=n;i++)t[x[i]=(s[i]-'0')]++;
for(RG int i=1;i<=m;i++)t[i]+=t[i-1];
for(RG int i=n;i;i--)sa[t[x[i]]--]=i;
for(RG int k=1,p;k<=n;k<<=1){
p=0;
for(RG int i=0;i<=m;i++)t[i]=y[i]=0;
for(RG int i=n-k+1;i<=n;i++)y[++p]=i;
for(RG int i=1;i<=n;i++)if(sa[i]>k)y[++p]=sa[i]-k;
for(RG int i=1;i<=n;i++)t[x[y[i]]]++;
for(RG int i=1;i<=m;i++)t[i]+=t[i-1];
for(RG int i=n;i;i--)sa[t[x[y[i]]]--]=y[i];
swap(x,y);x[sa[1]]=p=1;
for(RG int i=2;i<=n;i++)x[sa[i]]=(cmp(sa[i],sa[i-1],k)?p:++p);
if(p>=n)break;m=p;
}
}
后缀数组的一些性质
摘自:
http://hihocoder.com/problemset/problem/1403,
http://hihocoder.com/problemset/problem/1407,
http://hihocoder.com/problemset/problem/1415,
http://hihocoder.com/problemset/problem/1419,
有删改
Height数组
定义\(Height\)数组表示排名为\((i-1)\)的后缀和排名为\(i\)的后缀的最长公共前缀(Longest Common Prefix,LCP)。
即\(Height[i]=LCP(i-1,i)\)
1:若\(rank_j<rank_k\),则有\(LCP(j,k)=min_{i=1}^{rank[k]-rank[j]}Height[rank[j]+i]\)
这个性质显然。
因此,我们可以使用\(ST\)表在\(O(nlogn)+O(1)\)的时间内快速查询两个后缀的\(LCP\)
int S[21][N],p[N],lg[N],ans;
il void init(){
p[0]=1;for(RG int i=1;p[i-1]<=n;i++)p[i]=p[i-1]*2;
for(RG int i=1;i<=n;i++)lg[i]=lg[i>>1]+1;
for(RG int i=1;i<=n;i++)lg[i]--;
for(RG int i=1;i<=n;i++)S[0][i]=height[i];
for(RG int k=1;p[k]<=n;k++)
for(RG int i=1;i+p[k-1]<=n;i++)
S[k][i]=min(S[k-1][i],S[k-1][i+p[k-1]]);
}
il int lcp(int l,int r){
l=rk[l];r=rk[r];if(l>r)swap(l,r);l++;
return min(S[lg[r-l+1]][l],S[lg[r-l+1]][r-p[lg[r-l+1]-1]]);
}
2:\(Height[rank[i]]\ge Height[rank[i-1]]-1\)
假设将所有后缀排序后,第\((i-1)\)个后缀的前一个后缀是第\(k\)个后缀,
即\(rank[i-1]=rank[k]+1\),\(Height[rank[i-1]]=LCP(k,i-1)\)
我们可以知道第\((k+1)\)个后缀一定在第\(i\)个后缀的前面(此时\(Height[rank[i-1]]>1\)),
那么\(LCP(k+1,i)=LCP(k,i-1)-1=Height[rank[i-1]]-1\)
因此\(Height[rank[i-1]]-1=min_{j=1}^{rank[i]-rank[k+1]}Height[rank[k+1]+j]\)
所以有\(Height[rank[i]]\ge Height[rank[i-1]]-1\)
有了这个性质我们就可以再\(O(n)\)的时间内求出一个字符串的\(Height\)数组
这里是代码
int height[N],rk[N];//rk表示rank数组
il void getheight(){
for(RG int i=1;i<=n;i++){//求后缀i的height
height[rk[i]]=height[rk[i-1]]?height[rk[i-1]]-1:0;
while(a[sa[rk[i]-1]+height[rk[i]]]==a[i+height[rk[i]]])
height[rk[i]]++;
}
}
求区间内本质不同子串数目
考虑\(height\)数组是怎样体现重复子串的。
因为每个子串一定是原串后缀的一个前缀,那么当出现一对长为\(len\)的相同子串的时候,
这对相同子串各自的后缀就一定会有长为\(len\)的一段前缀相同。
而\(height\)数组正好表示的就是后缀排序中相邻前缀的\(lcp\)长度。
更为清晰的说法是,对于一个重复出现的子串,这些子串对应的后缀后缀排序后一定是连续的一段。
而\(height[i]\)记录的就是经过\(i\)的连续段的数量
那么可以知道答案就是\(\frac{n(n+1)}{2}-\sum_{i}height[i]\),实际就是将每一段拆开来求。
求最长可重叠重复\(K\)次字串
重复子串即两后缀的公共前缀,因此重复子串的最大长度即\(Height\)数组的最大值
(已经将后缀按照字典序排好,不可能有字典序不相邻的后缀其\(LCP\)反而更大的情况)
求重复\(K\)次字串的最大长度,即求\(Height\)数组长度为\(K\)的连续一段的最小值的最大值
使用单调队列即可完成
int f[N],g[N],l=1,r,ans;
int main()
{
n=read();k=read();
for(RG int i=1;i<=n;i++)a[i]=read();
getsa();getheight();
for(RG int i=1;i<=n;i++){
while(l<=r&&f[r]>=height[i])r--;
f[++r]=height[i];g[r]=i;
while(l<=r&&g[l]<=i-k+1)l++;
ans=max(ans,f[l]);
}
printf("%d\n",ans);
return 0;
}
求最长不可重叠重复子串问题
考虑二分答案;
检查字符串中是否有长度为\(K\)的不可重叠重复子串时,找到\(Height\le K\)的连续的一段,查出这段中后缀位置的最大值和最小值之差,
如果某一段的差值\(\le K\)则可行,否则不可行
il bool check(int k){
for(RG int i=1,mn=inf,mx=0;i<=n;i++){
if(height[i]>=k){
mn=min(mn,min(sa[i-1],sa[i]));
mx=max(mx,max(sa[i-1],sa[i]));
if(mx-mn>=k)return 1;
}
else{mn=inf;mx=0;}
}
return 0;
}
int main()
{
n=read();
for(RG int i=1;i<=n;i++)a[i]=read();
getsa();getheight();
RG int l=1,r=n,mid,ans=0;
while(l<=r){
RG int mid=((l+r)>>1);
if(check(mid))ans=mid,l=mid+1;
else r=mid-1;
}
printf("%d\n",ans);
return 0;
}
求最长公共字串
两个串
将这两个串拼到一起,中间使用一个不在两个串中出现过的字符,之后求出这个合并串的\(Height\)
那么最长公共字串就是不在同一个分串中的后缀\(Height\)的最大值
int main()
{
scanf("%s",s+1);n1=strlen(s+1);
s[++n1]='z'+1;scanf("%s",s+n1+1);n2=strlen(s+1);
for(RG int i=1;i<=n2;i++)a[i]=s[i]-'a'+1;
getsa();getheight();
for(RG int i=2;i<=n2;i++)
if((sa[i]<n1)^(sa[i-1]<n1))
ans=max(ans,height[i]);
printf("%d\n",ans);return 0;
}
多个串
仍然拼到一起,需要使用尺取法枚举左端点
求重复次数最多的连续字串
考虑枚举串长\(L\),可以知道\(k=LCP(i,i+L)/L+1\)就是以\(i\)开头,长度为\(L\)的循环次数
对于每次枚举,只考虑枚举\(L\)的倍数部分
如果不是\(L\)的倍数部分次数超过了\(k\),那么这个部分的结尾一定在
\([i+L*k,i+L*k-1+LCP(i,i+L)\%L]\)之间
此时\(LCP(i-(k-LCP(i,i+L)\%L),i+k-(k-LCP(i,i+L)\%L))\)一定只比\(k\)大\(1\)
于是每次枚举的复杂度为\(O(\frac{n}{L})\)
总复杂度是\(O(nlogn)\)
int main()
{
scanf("%s",s+1);n=strlen(s+1);
for(RG int i=1;i<=n;i++)a[i]=s[i]-'a'+1;
getsa();getheight();
init();
for(RG int k=1,len,els;k+k<=n;k++)
for(RG int i=k;i+k<=n;i+=k){
len=lcp(i,i+k);ans=max(ans,len/k+1);
if((els=lcp(i+len%k-k,i+len%k)/k+1)==len/k+2)
ans=max(ans,els);
}
printf("%d\n",ans);
return 0;
}
后缀数组SA学习笔记的更多相关文章
- 洛谷.3809.[模板]后缀排序(后缀数组 倍增) & 学习笔记
题目链接 //输出ht见UOJ.35 #include<cstdio> #include<cstring> #include<algorithm> const in ...
- 后缀数组SA入门(史上最晦涩难懂的讲解)
参考资料:victorique的博客(有一点锅无伤大雅,记得看评论区),$wzz$ 课件(快去$ftp$%%%),$oi-wiki$以及某个人的帮助(万分感谢!) 首先还是要说一句:我不知道为什么我这 ...
- 后缀数组(SA)总结
后缀数组(SA)总结 这个东西鸽了好久了,今天补一下 概念 后缀数组\(SA\)是什么东西? 它是记录一个字符串每个后缀的字典序的数组 \(sa[i]\):表示排名为\(i\)的后缀是哪一个. \(r ...
- [笔记]后缀数组SA
参考资料这次是真抄的: 1.后缀数组详解 2.后缀数组-学习笔记 3.后缀数组--处理字符串的有力工具 定义 \(SA\)排名为\(i\)的后缀的位置 \(rk\)位置为\(i\)的后缀的排名 \(t ...
- bzoj3796(后缀数组)(SA四连)
bzoj3796Mushroom追妹纸 题目描述 Mushroom最近看上了一个漂亮妹纸.他选择一种非常经典的手段来表达自己的心意——写情书.考虑到自己的表达能力,Mushroom决定不手写情书.他从 ...
- SA 学习笔记
后缀数组是解决字符串问题的有力工具--罗穗骞 后缀数组是对字符串的后缀排序的一个工具, sa将排名为i的字符串的开头位置记录下来, rnk将开头位置为i的字符串的排名记录下来. https://www ...
- 【字符串】后缀数组SA
后缀数组 概念 实际上就是将一个字符串的所有后缀按照字典序排序 得到了两个数组 \(sa[i]\) 和 \(rk[i]\),其中 \(sa[i]\) 表示排名为 i 的后缀,\(rk[i]\) 表示后 ...
- 浅谈后缀数组SA
这篇博客不打算讲多么详细,网上关于后缀数组的blog比我讲的好多了,这一篇博客我是为自己加深印象写的. 给你们分享了那么多,容我自私一回吧~ 参考资料:这位dalao的blog 一.关于求Suffix ...
- 后缀自动机SAM学习笔记
前言(2019.1.6) 已经是二周目了呢... 之前还是有一些东西没有理解到位 重新写一下吧 后缀自动机的一些基本概念 参考资料和例子 from hihocoder DZYO神仙翻译的神仙论文 简而 ...
随机推荐
- AOSP 设置编译输出目录
export OUT_DIR=/media/caoxinyu/TomasYu/out 注意:export OUT_DIR= OUT_DIR 后面直接跟= ,不要有空格.否则报错.
- springboot之redis的应用
1.redis是一个key-value存储系统.和Memcached类似,它支持存储的value类型相对更多,包括string(字符串).list(链表).set(集合).zset(sorted se ...
- JVM学习--jvm监控和故障处理工具
java虚拟机性能监控常用命令 Sun JDK监控和故障处理命令有jps.jstat.jinfo.jmap.jhat.jstack . 1.jps jps:JVM Process Status Too ...
- leetcode-电话号码的字母组合
电话号码的字母组合 给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合. 给出数字到字母的映射如下(与电话按键相同).注意 1 不对应任何字母. 示例: 输入:"23" ...
- Vue-cli 工具 / 通过 Vue-cli 工具重构 todoList
本博文归纳在 Vue 学习过程中, Vue-cli 工具的使用说明.除此之外还通过 Vue-cli 工具将之前 Vuejs 基本语法当中实现的 todoList 进行重构. 安装 npm instal ...
- Faster RCNN论文解析
Faster R-CNN由一个推荐区域的全卷积网络和Fast R-CNN组成, Fast R-CNN使用推荐区域.整个网络的结构如下: 1.1 区域推荐网络 输入是一张图片(任意大小), 输出是目标推 ...
- node.js安装部署
node js 安装部署学习 CentOS 下安装 Node.js 1.下载源码,你需要在https://nodejs.org/en/download/下载最新的Nodejs版本,链接: http ...
- encode 与 decode
decode 将其它编码的字符串转换成unicode编码,例如:str1.decode("gb2312"),表示将gb2312编码的字符串转换成unicode编码 encode 将 ...
- OSG配置捷径,VS2013+WIN10
在自己电脑上用CMAKE已经编译好了,上传到百度云里面了. 环境是WIN10+VS2013. 链接:http://pan.baidu.com/s/1hrO7GFE 密码:fwkw 解压之后放在C盘或者 ...
- 某一线互联网公司前端面试题js部分总结
js部分 1,使用严格模式的优点 - 消除Javascript语法的一些不合理.不严谨之处,减少一些怪异行为; - 消除代码运行的一些不安全之处,保证代码运行的安全: - 提高编译器效率,增加运行速度 ...