后缀数组SA学习笔记

什么是后缀数组

后缀数组(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]))的排名...直到(pge 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
",ans);
	return 0;
}

求最长不可重叠重复子串问题

考虑二分答案;
检查字符串中是否有长度为(K)的不可重叠重复子串时,找到(Heightle 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
",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
",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
",ans);
	return 0;
}
原文地址:https://www.cnblogs.com/cjfdf/p/9338220.html