什么是后缀数组

后缀数组\(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. 洛谷.3809.[模板]后缀排序(后缀数组 倍增) & 学习笔记

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

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

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

  3. 后缀数组(SA)总结

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

  4. [笔记]后缀数组SA

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

  5. bzoj3796(后缀数组)(SA四连)

    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. webpack loader 生成虚拟文件的方案

    此文已由作者张磊授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. 前言 使用 webpack 的时候,难免需要写一些 loader,接着就会遇到一个很纠结的问题.该 loade ...

  2. 基于Redis+Kafka的首页曝光过滤方案

    本文来自网易云社区 作者:李勇 背景 网易美学首页除了banner和四个固定位,大部分都是通过算法推荐获取的内容,其中的内容包括心得.合辑.视频及问答等.现在需要实现的是当推荐内容在用户屏幕曝光后(即 ...

  3. Android Preference 设置偏好全攻略

    Android 设置是每个App必不可小的东西,看似很简单,但是初学不熟悉的很花时间去研究,特别样式兼容方面,以及有自定义设置的需求,下面是对用法做一个总结 Preference结构 界面结构看下图 ...

  4. Web应用服务器性能压力测试

    压力测试需要关注三个方面:如何正确产生压力.如何定位瓶颈.如何预估系统的承载能力 产生压力的方法 通常可以写脚本产生压力机器人对服务器进行发包和收包操作,也可以使用现有的工具(像jmeter.Load ...

  5. Jenkins 配置邮箱 530Authentication required ,535 uthentication failed 的解决方法

      错误 解决方法 530 Authentication required  需要展开SMTP认证,输入SMTP server能识别的用户信息 535 authentication failed  输 ...

  6. python函数参数默认值及重要警告

    最有用的形式是对一个或多个参数指定一个默认值.这样创建的函数,可以用比定义时允许的更少的参数调用,比如: def ask_ok(prompt, retries=4, reminder='Please ...

  7. leetcode-岛屿的个数

    岛屿的个数 给定一个由 '1'(陆地)和 '0'(水)组成的的二维网格,计算岛屿的数量.一个岛被水包围,并且它是通过水平方向或垂直方向上相邻的陆地连接而成的.你可以假设网格的四个边均被水包围. 示例 ...

  8. (python)leetcode刷题笔记05 Longest Palindromic Substring

    5. Longest Palindromic Substring Given a string s, find the longest palindromic substring in s. You ...

  9. spark集群安装部署

    通过Ambari(HDP)或者Cloudera Management (CDH)等集群管理服务安装和部署在此不多介绍,只需要在界面直接操作和配置即可,本文主要通过原生安装,熟悉安装配置流程. 1.选取 ...

  10. 如何让thinkpad X1C 用U盘 安装上专业版win10

    1 BIOS内置了文件 会导致win10 iso默认装家庭版 2 给iso 的resouse 目录中增加文件ei.cfg 3 内容如下 [EditionID]Professional[Channel] ...