从SA入门到SAM精通

SA

基本应用

读入一个长度为 n 的由大小写英文字母或数字组成的字符串,请把这个字符串的所有非空后缀按字典序(用 ASCII 数值比较)从小到大排序。

解法

1.将每个后缀取出来,直接排序 O(n2logn)
2.用hash二分LCP比较下一位,O(nlog2n)
3.倍增求后缀数组,O(nlogn)
4.高级方法求后缀数组,O(n)

倍增

先比较每个后缀的第一位,再比较前两位,前四位...
问题在于如何快速比较前两位,前四位。
一个有趣的性质是在比较2k位时,我们知道2k1位的大小,所以2k位的大小只与前一半2k1和后一半2k1有关,所以可以用基数排序由上一层推到这一层。
image

基数排序

正常基数排序,是按数位从高到低依次比较大小,比如说三位数,就先比较百位的数字,将百位为 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[i1]),现在先想如何求 height 数组。感觉也不好求。
那我们先研究它的性质,我们可以发现 height[rk[i]1]1height[rk[i]] (发现不了一点) 考虑如何证明。
image
我们先将从 i1nkn 的后缀取出来.(rk[k]=rk[i1]1)。图中的黄色部分就是它们的LCP,如果lcp1 上面的式子显然成立,考虑大于 1 的情况。
image
我们可以去掉开头字母,则 k+1i的lcp为 height[rk[i]1]1,所以height[i] 至少为 height[rk[i]1]1,因为只有可能出现下图的情况或就是k+1.
image
这样就可以继承前面的信息暴力改。

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,最多加 2nn 所以时间复杂度是 O(n)
回到最开始的地方,现在一直 height 如何求任意后缀LCP,显然是它们的最小值(感性理解即可,不理解就是没有理解后缀数组多看几遍)。所以可以用st表维护最小值。

可重叠最长重复子串

height的最大值。
规定长度就是区间 height 的最小值的最大值:P2852

不可重叠最长重复子串

首先二分答案 x, 对height数组进行分组,保证每一组最小都大于 x(有x的长度)
依次枚举每一组,记录下最大和最小长度,如果相减大于x(不重叠)那么可以更新答案。

本质不同子串

子串是后缀的前缀,后缀拍完序之后,每次新增的只有除LCP以外的子串。
所以总数为 n(n+1)2i=1nheight[i] P2408

比较子串大小

求出两子串所属的后缀的LCP,如果LCP比长度大则一个是另一个的子串,所以按长度即可比较大小,如果不是,则直接根据 rk 比较大小。

更多

SAM

字符串的 SAM 可以理解为给定字符串的所有子串的压缩形式。值得注意的事实是,SAM 将所有的这些信息以高度压缩的形式储存。对于一个长度为 n$ 它的空间复杂度仅为 O(n), 时间复杂度O(n) 。(准确的所是O(nlogm),m为字符集大小。

posted @   storms11  阅读(10)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示