后缀数组学习笔记
1. 前置知识:基数排序
1.1. 思想
现有如下序列:3,44,38,5,47,15,36,32,50,现在要用基数排序算法排序,要怎么做?
基数排序的初始状态如下:
- 按照个位将原序列中的数分组,放入对应的集合
- 将分好的数按照个位的顺序取出,得到:
- 将序列中的数重新按照十位分组,放入对应集合:
- 将每一位上的数按从下到上的顺序依次取出,就是答案
基数排序利用的是一个桶思想,属于非比较算法
在数更多或位数更多的情况下,重复此过程即可
1.2. 代码:
#include<cstdio>
#include<algorithm>
using namespace std;
int n,a[105],cnt[15],b[105];
int main()
{
scanf("%d",&n);
int mx=0;
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
mx=max(mx,a[i]);
}
int d=0;
while(mx>0)
{
mx/=10;
d++;
}
int tmp=1;
for(int i=1;i<=d;i++)
{
for(int j=0;j<10;j++) cnt[j]=0;
for(int j=1;j<=n;j++)
{
int k=(a[j]/tmp)%10;
cnt[k]++;
}
for(int j=1;j<10;j++)
{
cnt[j]+=cnt[j-1];
}
for(int j=n;j>0;j--)
{
int k=(a[j]/tmp)%10;
b[cnt[k]]=a[j];
cnt[k]--;
}
for(int j=1;j<=n;j++)
{
a[j]=b[j];
}
tmp*=10;
}
for(int i=1;i<=n;i++)
{
printf("%d ",a[i]);
}
return 0;
}
2.基本概念
后缀:是指从某一个位置i开始直到整个串末尾的某个子串
后缀数组:用\(sa_i\)表示,指所欲哦后缀在排完序后,排名为i的串在原串中的位置,通俗的讲,就是\(sa[排名]=位置\)
名次数组:用\(rank_i\)表示,是指所有后缀在排完序后,原字符串中第i个后缀现在的排名,即\(rank[位置]=排名\)
以\(aabaaaab\)为例,它的后缀,后缀数组,名次数组如下:
字符串的大小比较:
字符串比较是逐位按字典序比较,若字典序相同,则比较下一位,否则直接分出大小,例如:
\(b>aaaaaaa\)
\(aab<aabc\)
3. 倍增求后缀数组
3.1. 思想
这里倍增比较字符串的长度
第一次是比较长度为1的字符串
第二次比较的是长度为2的字符串,可以用一个窗口\([l,l+1]\)来表示这个字符串,显然,这个字符串是由2个相邻且长度为1的字符串拼接而成的,长度为2的字符串的排名是由两个长度为1的字符串的排名x和y组成xy
第三次比较的是长度为4的字符串,可以用一个窗口\([l,l+3]\)来表示这个字符串,显然,这个字符串是由2个相邻且长度为2的字符串拼接而成的,长度为4的字符串的排名是由两个长度为2的字符串的排名x和y组成xy
依次类推
第\(k\)次比较的是长度为\(2^{k-1}\)的字符串,可以用一个窗口\([l,l+2^{k-1}-1]\)来表示这个字符串,显然,这个字符串是由2个相邻且长度为\(2^{k-2}\)的字符串拼接而成的,长度为2的字符串的排名是由两个长度为\(2^{k-2}\)的字符串的排名x和y组成xy
如何通过排名来比较字符串大小?
举个例子,两个长度为4的后缀str1和str2:
str1由两个长度为2的字符串拼成,他们的排名为x1和y1,str2由两个长度为2的字符串拼成,他们的排名为x2和y2
此时比较str1和str2的大小,可以以x为第一关键字,若\(x1=x2\),则比较y,即y为第二关键字
注意:
-
在比较的过程中,如果后续的字符不够,则用0来补足
-
当\(2^{k-1}\ge n\)时,就会得出答案
具体比较过程如图:
这里的两个关键字,就相当于数字中的十位和个位,所以排序不分可以所以基数排序,倍增的时间复杂度为\(O(\log n)\),所以总时间复杂度为\(O(n \log ^2 n)\)
3.2. 例题
题目背景
这是一道模板题。
题目描述
读入一个长度为 $ n $ 的由大小写英文字母或数字组成的字符串,请把这个字符串的所有非空后缀按字典序(用 ASCII 数值比较)从小到大排序,然后按顺序输出后缀的第一个字符在原串中的位置。位置编号为 $ 1 $ 到 $ n $。
输入格式
一行一个长度为 $ n $ 的仅包含大小写英文字母或数字的字符串。
输出格式
一行,共 \(n\) 个整数,表示答案。
样例 #1
样例输入 #1
ababa
样例输出 #1
5 3 1 4 2
提示
\(1\le n \le 10^6\)。
3.3. 代码
#include<cstdio>
#include<algorithm>
#include<string>
#include<iostream>
#include<cstring>
using namespace std;
const int N=2e6+5;
string s;
int n,x[N],y[N],cnt[N],sa[N],m;
int main()
{
cin>>s;
n=s.size();
s=" "+s;
m=122;
for(int i=1;i<=n;i++) cnt[x[i]=s[i]]++;
for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
for(int i=n;i>0;i--) sa[cnt[x[i]]--]=i;
for(int k=1;k<=n;k<<=1)
{
memset(cnt,0,sizeof(cnt));
for(int i=1;i<=n;i++) y[i]=sa[i];
for(int i=1;i<=n;i++) cnt[x[y[i]+k]]++;
for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
for(int i=n;i>0;i--) sa[cnt[x[y[i]+k]]--]=y[i];
memset(cnt,0,sizeof(cnt));
for(int i=1;i<=n;i++) y[i]=sa[i];
for(int i=1;i<=n;i++) cnt[x[y[i]]]++;
for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
for(int i=n;i>0;i--) sa[cnt[x[y[i]]]--]=y[i];
for(int i=1;i<=n;i++) y[i]=x[i];
m=0;
for(int i=1;i<=n;i++)
{
if(y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k])
{
x[sa[i]]=m;
}
else x[sa[i]]=++m;
}
if(m==n) break;
}
for(int i=1;i<=n;i++)
{
printf("%d ",sa[i]);
}
return 0;
}
3.4. 后缀数组的应用
3.4.1. height数组
height数组:定义\(height_i=suffix(sa_{i-1})\)和\(suffix(sa_i)\)的最长公共前缀,也就是排名相邻的两个后缀的最长公共前缀,记作\(height_i=lcp(sa_i,sa_{i-1})\)
如果按照\(height_1\)至\(height_n\)的顺序计算,时间复杂度\(O(n^2)\),没有用到字符串的性质
有一个性质:
证明:
当\(height[rank[i-1]]\le 1\)时,上式显然成立(右边小于等于 0)
当\(height[rank[i-1]]>1\)时:
根据\(height\)定义,有\(lcp(sa[rank[i-1]], sa[rank[i-1]-1]) = height[rk[i-1]] > 1\)
既然后缀\(i-1\)和后缀\(sa[rank[i-1]-1]\)有长度为\(height[rank[i-1]]\)的最长公共前缀,那么不妨用\(aA\)来表示这个最长公共前缀,其中a是一个字符,A是长度为\(height[rank[i-1]]-1\)的字符串
那么后缀\(i-1\)可以表示为\(aAD\),后缀\(sa[rank[i-1]-1]\)可以表示为$aAB。B<D,B可能为空串,D非空
进一步地,后缀\(i\)可以表示为\(AD\),存在后缀\((sa[rk[i-1]-1]+1)AB\)
因为后缀\(sa[rk[i]-1]\)在大小关系的排名上仅比后缀\(sa[rk[i]]\)也就是后缀i,小一位,而AB < AD。所以\(AB \leqslant\)后缀\(sa[rk[i]-1] < AD\),显然后缀i和后缀\(sa[rk[i]-1]\)有公共前缀 A。
于是就可以得出\(lcp(i,sa[rk[i]-1])\)至少是\(height[rk[i-1]]-1\),也即\(height[rk[i]]\ge height[rk[i-1]]-1\)。
所以,在求解时,就可以用着个性质,从前往后暴力匹配即可
代码:
void get_height()
{
for(int i=1;i<=n;i++) rk[sa[i]]=i;
for(int i=1,k=0;i<=n;i++)
{
if(rk[i]==1) continue;
if(k) k--;
int j=sa[rk[i]-1];
while(i+k<=n&&j+k<=n&&s[i+k]==s[j+k]) k++;
height[rk[i]]=k;
}
}
同时,如果要求两个排名不连续的串的最长公共前缀,则直接取这段排名区间的最小值
例如aabaaaab中,求abaaaab和aaab的最长公共前缀,如图:
注意,最后求出来的是\(height[排名]=长度\)
3.4.2. 最长重复子串(可重叠)
显然,height所求的公共前缀必然出现了两次,所以求出所有height的最大值即可
4. 例题:
4.1. [JSOI2007] 字符加密
https://gxyzoj.com/d/gxyznoi/p/101
看到字符串排序,想后缀数组,但是如果直接排序,后面的子串就会因为不完整出现排序错误
所以,可以先将子串复制两次,然后在求sa
4.2. [luogu2408] 不同子串个数
https://gxyzoj.com/d/gxyznoi/p/102
当两个子串相等时,那么着两个子串必然是两个后缀的公共前缀
所以考虑后缀数组,排序后求出height数组,此时,如果子串s是排名相邻串的最长公共前缀,则可以直接减去长度
但是如果不相邻,则必然时其他两个相邻串的非最长公共前缀,所以在前面的统计中必然不会遗漏
4.3. [ural1297] Palindrome
https://gxyzoj.com/d/gxyznoi/p/103
从暴力开始,最简单的方法显然是枚举起点和终点,然后匹配,显然会T
考虑到,在一个回文串中,显然存在一个对称点,而将原串反转后,在原串和新串中,从对称点开始的两个后缀的公共前缀就是回文串一半(向上取整)的长度
所以可以将原串和新串拼起来,中间加一个不会出现的字符即可,记长度为n
根据上面的性质,对于中心是一个字符,求\(lcp(s[rank[i]],s[rank[n-i+1]])\),对于中心不是字符求\(lcp(s[rank[i]],s[rank[n-i]])\)
4.4. [Poi2000] 公共串
https://gxyzoj.com/d/gxyznoi/p/P104
注意,是求所有串的公共子串!!!
在这些串中,任选各任选一个点,那么他们后缀的公共前缀就是公共子串
所以根据上面一道题,可以讲所有子串拼起来,中间用未出现的字符隔开,然后求后缀数组和height数组
接下来,因为任意两串的公共前缀是排名区间内取min,所以名次间隔越小,值就越大
可以使用双指针,当满足每一个子串都出现过时,求min即可
4.5. [USACO06DEC] Milk Patterns G
https://gxyzoj.com/d/gxyznoi/p/P105
要求的是重复出现的子串的最长长度,根据样例,是可重叠的,而且有出现次数的限制
可以借鉴3.4.2的思路,因为次数至少为k,所以当次数为x+1的答案为t时,次数为x的答案必然大于等于t
所以,可以找排名连续的k个后缀中的公共前缀的最小值,再去所有最小值的max,显然可以用单调队列
4.6. [SDOI2008] Sandy的卡片
https://gxyzoj.com/d/gxyznoi/p/P106
因为是在一个串中的一段同时加上同一个值之后,与其他串中经过同样处理后的某一部分相同
所以考虑差分,此时,相同的串在差分后必然相等,此时,这道题就变成了[Poi2000] 公共串
注意,因为比较的是差分数组,所以要加1
4.7. [Ahoi2013] 差异
https://gxyzoj.com/d/gxyznoi/p/P109
主要在于求lcp,直接for循环显然会T
因为lcp是一段数中的最小值,所以可以转换思路,由求一段的最小值变为求又多少种区间的最小值为当前数字
显然单调栈,注意,因为存在相等,所以边界要一边小于等于一边小于
4.8. [bzoj3230] 相似子串
https://gxyzoj.com/d/gxyznoi/p/P110
思路其实不难,因为是求前后的公共前缀,所以很自然的想到可以另外建一个反串,求它的sa
所以,现在的关键在于如何求出它的起始和终止位置的排名
可以记录从排名1到n的不同的前缀数的前缀和,即:\(num_i=num_{i-1}+n-sa_{i+1}-ht_i\)
所以,可以二分解决起点问题,对于终点,因为同起点的串的排名是相邻的,而长度越短的越靠前,所以直接找到终点的起点相加即可
4.9. [NOI2015] 品酒大会
因为根据相似度的定义,就是求两个串的最长公共前缀
先考虑第一个问题
依题意得,因为当然两杯“\(r\)相似”\((r>1)\)的酒同时也是“\(1\)相似”、“\(2\)相似”、……、“\((r−1)\)相似”的,所以可以求出每个公共前缀恰好为x的数量后求前缀和
而公共前缀恰好是x,则要求这两个串的排名之间的最小height是x
转换思路,用height考虑,因为要求最小值,考虑将height按照从大到小的顺序排序,然后依次加入
当加入一个height后,必然会将两个区域连接起来,而所有在左右各选一个点所构成的区间,最小值必然是当前值
这时只需要将左右的个数乘起来即可,因为涉及区间的合并,可以采用并查集
接下来考虑第二个问题
显然是在左和右各选一个点的值相乘,但是因为存在负数,所以还要记录最小值