后缀数组SA学习笔记

时间:2022-12-24 20:15:07

什么是后缀数组

后缀数组\(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学习笔记的更多相关文章

  1. 洛谷&period;3809&period;&lbrack;模板&rsqb;后缀排序&lpar;后缀数组 倍增&rpar; &amp&semi; 学习笔记

    题目链接 //输出ht见UOJ.35 #include<cstdio> #include<cstring> #include<algorithm> const in ...

  2. 后缀数组SA入门(史上最晦涩难懂的讲解)

    参考资料:victorique的博客(有一点锅无伤大雅,记得看评论区),$wzz$ 课件(快去$ftp$%%%),$oi-wiki$以及某个人的帮助(万分感谢!) 首先还是要说一句:我不知道为什么我这 ...

  3. 后缀数组&lpar;SA&rpar;总结

    后缀数组(SA)总结 这个东西鸽了好久了,今天补一下 概念 后缀数组\(SA\)是什么东西? 它是记录一个字符串每个后缀的字典序的数组 \(sa[i]\):表示排名为\(i\)的后缀是哪一个. \(r ...

  4. &lbrack;笔记&rsqb;后缀数组SA

    参考资料这次是真抄的: 1.后缀数组详解 2.后缀数组-学习笔记 3.后缀数组--处理字符串的有力工具 定义 \(SA\)排名为\(i\)的后缀的位置 \(rk\)位置为\(i\)的后缀的排名 \(t ...

  5. bzoj3796&lpar;后缀数组&rpar;&lpar;SA四连&rpar;

    bzoj3796Mushroom追妹纸 题目描述 Mushroom最近看上了一个漂亮妹纸.他选择一种非常经典的手段来表达自己的心意——写情书.考虑到自己的表达能力,Mushroom决定不手写情书.他从 ...

  6. SA 学习笔记

    后缀数组是解决字符串问题的有力工具--罗穗骞 后缀数组是对字符串的后缀排序的一个工具, sa将排名为i的字符串的开头位置记录下来, rnk将开头位置为i的字符串的排名记录下来. https://www ...

  7. 【字符串】后缀数组SA

    后缀数组 概念 实际上就是将一个字符串的所有后缀按照字典序排序 得到了两个数组 \(sa[i]\) 和 \(rk[i]\),其中 \(sa[i]\) 表示排名为 i 的后缀,\(rk[i]\) 表示后 ...

  8. 浅谈后缀数组SA

    这篇博客不打算讲多么详细,网上关于后缀数组的blog比我讲的好多了,这一篇博客我是为自己加深印象写的. 给你们分享了那么多,容我自私一回吧~ 参考资料:这位dalao的blog 一.关于求Suffix ...

  9. 后缀自动机SAM学习笔记

    前言(2019.1.6) 已经是二周目了呢... 之前还是有一些东西没有理解到位 重新写一下吧 后缀自动机的一些基本概念 参考资料和例子 from hihocoder DZYO神仙翻译的神仙论文 简而 ...

随机推荐

  1. WIN32 API编程之 透明static

    createwindow可以直接创建一个staitc,但这个static是不透明的,如果我们把窗口背景设置为GRAY_BRUSH,则static会很明显的有一个白色背景,一般来说这样肯定很难看. 可以 ...

  2. webpack练手项目之easySlide(一):初探webpack (转)

    最近在学习webpack,正好拿了之前做的一个小组件,图片轮播来做了下练手,让我们一起来初步感受下webpack的神奇魅力.     webpack是一个前端的打包管理工具,大家可以前往:http:/ ...

  3. 浅谈C&num;浅拷贝和深拷贝

    近来爱上一本书<编写高质量代码,改善C#程序的157个建议>,我想很多人都想编写高质量的代码,因为我们不仅仅是码农,更是一名程序员. 从今天开始,我将每天和大家分享这本书中的内容,并加上自 ...

  4. Codeforces Educational Codeforces Round 3 B&period; The Best Gift 水题

    B. The Best Gift 题目连接: http://www.codeforces.com/contest/609/problem/B Description Emily's birthday ...

  5. wpf 只在window是ShowDialog打开时才设置DialogResult

    //only set DialogResult when window is ShowDialog before if(System.Windows.Interop.ComponentDispatch ...

  6. opencv 2&period;46与visual studio 2012 配置方法

    一开学就搞实训,还是没学过的图像处理.痛苦啊!图像处理时一般使用Matlab中的图像工具箱,或者是C/C++和OpenCV结合使用.以前看过一些关于opencv的文章,没想到现在要用上了. 把搭建开发 ...

  7. 老李分享:Uber究竟是用什么开发语言? 2

    Uber的任务分派系统是运行在Node上,这是一个运行在服务器端的JavaScript平台.当一个客户打开app或者网站来进行车辆预定或者调用其他的API来查看可用车辆信息的时候,大部分的这些服务都是 ...

  8. 跨域的另一种解决方案CORS(CrossOrigin Resource Sharing)跨域资源共享

    在我们日常的项目开发时使用AJAX,传统的Ajax请求只能获取在同一个域名下面的资源,但是HTML5打破了这个限制,允许Ajax发起跨域的请求.浏览器是可以发起跨域请求的,比如你可以外链一个外域的图片 ...

  9. Python&lowbar;字典及其操作

    字典 概念 字典,Python基础数据类型之一,{}以键值对的形式存储数据. 以key : value 形式存储数据.例如,name 为 key,Laonanhai 为 value. dic = {' ...

  10. PHP的curl查看header信息的功能(包括查看返回header和请求header)

    PHP的curl功能十分强大,简单点说,就是一个PHP实现浏览器的基础. 最常用的可能就是抓取远程数据或者向远程POST数据.但是在这个过程中,调试时,可能会有查看header的必要. 如下: ech ...