从SA入门到SAM精通
SA
基本应用
读入一个长度为 $ n $ 的由大小写英文字母或数字组成的字符串,请把这个字符串的所有非空后缀按字典序(用 ASCII 数值比较)从小到大排序。
解法
1.将每个后缀取出来,直接排序 \(O(n^2 \log n)\)
2.用hash二分LCP比较下一位,\(O(n \log^2 n)\)
3.倍增求后缀数组,\(O(n \log n)\)
4.高级方法求后缀数组,\(O(n)\)
倍增
先比较每个后缀的第一位,再比较前两位,前四位...
问题在于如何快速比较前两位,前四位。
一个有趣的性质是在比较\(2^k\)位时,我们知道\(2^{k-1}\)位的大小,所以\(2^k\)位的大小只与前一半\(2^{k-1}\)和后一半\(2^{k-1}\)有关,所以可以用基数排序由上一层推到这一层。
基数排序
正常基数排序,是按数位从高到低依次比较大小,比如说三位数,就先比较百位的数字,将百位为 \(0\) 的放在一起,将百位为 \(1\) 的放在一起...。然后,对十位进行比较,在百位为 \(0\) 的里面把十位为 \(0\) 的放在一起,十位为 \(1\) 的放在一起...,最后所有数都有序。
SA的基数排序,就是相当于只有两位数来排序。
代码实现
代码比较抽象要多理解,多思考
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int n,m,sa[N],rk[N],x[N],y[N],cnt,num;
char s[N];
void SA()
{
for(int i=1;i<=n;i++)rk[x[i]=s[i]]++;//rk辅助数组,x是上一层的排名
for(int i=1;i<=m;i++)rk[i]+=rk[i-1];
for(int i=n;i>=1;i--)sa[rk[x[i]]--]=i;//正序倒序都可以,sa是排名为i的后缀的起始下标
for(int k=1;k<=n;k<<=1)
{
cnt=0;
for(int i=n-k+1;i<=n;i++)y[++cnt]=i;//没有后一半是最强的,最靠前的
for(int i=1;i<=n;i++)if(sa[i]>k)y[++cnt]=sa[i]-k;//如果可以做后一半,就做
//正序枚举,因为y的顺序是后一半从小到大的顺序
for(int i=1;i<=m;i++)rk[i]=0;//清零
for(int i=1;i<=n;i++)rk[x[i]]++;//根据前一半
for(int i=1;i<=m;i++)rk[i]+=rk[i-1];
for(int i=n;i>=1;i--)sa[rk[x[y[i]]]--]=y[i],y[i]=0;//后一半更大的在前一半相同时排后面
swap(x,y);//y临时存一下上一层x的值。
x[sa[1]]=1,num=1;
for(int i=2;i<=n;i++)
{
x[sa[i]]=(y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k])?num:++num;//确定这一层的排名
}
if(num==n)break;//分完了
m=num;
}
for(int i=1;i<=n;i++)cout<<sa[i]<<' ';
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>s+1;
n=strlen(s+1),m=150;
SA();
return 0;
}
进阶应用
LCP与hieght数组
LCP:最长公共前缀,\(lcp(i,j)\) 表示字符串 \(i\) 与字符串 \(j\) 的最长公共前缀。
思考题:如果一直\(sa\) 数组如何快速求任意两个后缀的最长公共前缀。(想不出来是正常的不然还有什么讲的必要)
要先引入一个新数组 \(height\),定义\(height[i]=lcp(sa[i],sa[i-1])\),现在先想如何求 \(height\) 数组。感觉也不好求。
那我们先研究它的性质,我们可以发现 \(height[rk[i]-1]-1 \le height[rk[i]]\) (发现不了一点) 考虑如何证明。
我们先将从 \(i-1\) 到 \(n\) 和 \(k\) 到 \(n\) 的后缀取出来.(\(rk[k]=rk[i-1]-1\))。图中的黄色部分就是它们的LCP,如果\(lcp\le 1\) 上面的式子显然成立,考虑大于 \(1\) 的情况。
我们可以去掉开头字母,则 \(k+1\) 与 \(i\)的lcp为 \(height[rk[i]-1]-1\),所以\(height[i]\) 至少为 \(height[rk[i]-1]-1\),因为只有可能出现下图的情况或就是\(k+1\).
这样就可以继承前面的信息暴力改。
void geth()
{
int kk=0;
for(int i=1;i<=n;i++)x[sa[i]]=i;
for(int i=1;i<=n;i++)
{
if(x[i]==1)continue;
if(kk)kk--;
int j=sa[x[i]-1];
while(i+kk<=n&&j+kk<=n&&s[i+kk]==s[j+kk])kk++;
hi[x[i]]=kk;
}
}
时间复杂度,\(k\) 不会超过 \(n\),最多加 \(2n\) 减 \(n\) 所以时间复杂度是 \(O(n)\)。
回到最开始的地方,现在一直 \(height\) 如何求任意后缀LCP,显然是它们的最小值(感性理解即可,不理解就是没有理解后缀数组多看几遍)。所以可以用st表维护最小值。
可重叠最长重复子串
\(height\)的最大值。
规定长度就是区间 \(height\) 的最小值的最大值:P2852
不可重叠最长重复子串
首先二分答案 \(x\), 对height数组进行分组,保证每一组最小都大于 \(x\)(有x的长度)
依次枚举每一组,记录下最大和最小长度,如果相减大于\(x\)(不重叠)那么可以更新答案。
本质不同子串
子串是后缀的前缀,后缀拍完序之后,每次新增的只有除LCP以外的子串。
所以总数为 \(\frac{n*(n+1)}{2}-\sum_{i=1}^{n}height[i]\) P2408
比较子串大小
求出两子串所属的后缀的LCP,如果LCP比长度大则一个是另一个的子串,所以按长度即可比较大小,如果不是,则直接根据 \(rk\) 比较大小。