后缀数组SA

发布时间 2023-10-12 21:36:41作者: lzajty

后缀数组SA

\(rank[i]\):后缀i~n的排名

\(sa[i]\):排名为i的后缀的起始下标

\(h[i]\):起始下标为i的后缀和比它前一名的后缀的最长公共前缀

\(height[i]\) 排名为i的后缀和比它前一名的后缀的最长公共前缀

求sa数组

\(Olog(n)\)

代码:


#include<bits/stdc++.h>
using namespace std;
const int N=1000010;
int rr[N],rr2[N],sa[N];//rr[N]:第二关键字,rr[rr2[N]]第一关键字 
int c[N];//桶 
int cnt,n,m; 
string a;
int main()
{
	cin>>a;
	m='z';//m代表不同排名的个数,即桶的范围 
	n=a.size();
	for(int i=1;i<=n;i++)rr[i]=a[i-1];
	for(int i=1;i<=n;i++)c[rr[i]]++;//插入桶 
	for(int i=1;i<=m;i++)c[i]+=c[i-1];
	for(int i=n;i>=1;i--)sa[c[rr[i]]--]=i;//倒序枚举!!! 
	for(int k=1;k<=n;k<<=1){ 
		memset(c,0,sizeof c);//不能漏 
		cnt=0;
		//第一次相当于将每个值对应的(a,b)中的b排序 ,第二次则以a来 
		for(int i=n-k+1;i<=n;i++)rr2[++cnt]=i;//不存在第二关键字,相当于b=0,在第一次排序中应该排到最前面 
		for(int i=1;i<=n;i++)if(sa[i]>k)rr2[++cnt]=sa[i]-k;//改为第一关键字 
		for(int i=1;i<=n;i++)c[rr[i]]++;
		for(int i=1;i<=m;i++)c[i]+=c[i-1];
		for(int i=n;i>=1;i--)sa[c[rr[rr2[i]]]--]=rr2[i],rr2[i]=0; //rr2[i]不能写成i,因为是按rr2[1~n]在排序 
		cnt=0;
		swap(rr,rr2);//更新rr的值,暂时将rr放到rr2中 
		rr[sa[1]]=++cnt;
		for(int i=2;i<=n;i++){
			if(rr2[sa[i]]==rr2[sa[i-1]]&&rr2[sa[i]+k]==rr2[sa[i-1]+k])rr[sa[i]]=cnt;//是sa[i],因为rr[i]的i的意义为a的下标 
			else rr[sa[i]]=++cnt;
		}
		if(cnt==n)break;//排名均不相同 
		m=cnt;
	}
	for(int i=1;i<=n;i++)cout<<sa[i]<<" ";
	cout<<endl;
	return 0;
}

原理:

直接排序是O(n2)级别。。。

利用倍增思想,每次给以i为起始,长度为k(k倍增)的区间排序。

排序过程

当每次k倍增时,可以发现只需要对其的两个关键字进行基数排序即可。

第一个关键字为rank[i],第二个关键字为rank[i+k]如果没有,说明已经排好序,为0。

rank自然也求出来了:\(rank[sa[i]]=i\)


求h[i]:

定理:\(h[i] \geq h[i-1]-1\)

证明:(不严谨)

假如 \(ra[rank[i]-1]=ra[rank[i-1]-1]\) (即 \(h[i]\)\(h[i-1]\) 要匹配的后缀相同)

则若 \(h[i-1]\) 相当于 后缀 \(a\)~\(n\)\(b\)~\(n\) 匹配,

\(h[i]\) 相当于后缀 \(a\)~\(n\)\(b+1\)~\(n\) 匹配(因为只是前进了一位)

此时 \(h[i]=h[i-1]-1\)

而我们知道,上述假设是不可能成立的,\(rank[i]-1\) 不可能与 \(rank[i-1]-1\) 相同

所以 \(ra[rank[i-1]-1]\) 只是可能与 \(ra[i]\) 排名相邻

根据\(height\)数组的性质,排名相邻的匹配最大。

所以 $h[i] \geq h[i-1]-1 $


代码:

(rr为rank,hi为h)

	for(int i=1;i<=n;i++){
		int j=sa[rr[i]-1];
		int tt=hi[i-1]-1;
		if(tt<0)tt=0;
		hi[i]=tt;
		for(int l1=i+tt,l2=j+tt;l1<=n,l2<=n;l1++,l2++){
			if(a[l1]!=a[l2])break;
			hi[i]++;
		}
	}

height也出来了。


RMQ求height的区间最小值(即可求出任意后缀的LCP)

(dbc)


应用

1.求可重叠最长字串

只需求height数组的最大值即可。


2.不可重叠最长重复子串

满足 \(sa[i]-sa[j]\geq \operatorname{LCP}(i,j)\)(后缀\(i,j\)的最长公共前缀)

可以二分长度,每次判定是否有\(\geq k\)长度的方案。(转化为判定

然后有一个非常重要的思想:分组

每一组中都满足 \(hetght[i]\geq k\)

则我们记录每个组中 \(sa[i]\) 的最大值\(a\) 和最小值 \(b\)

\(a-b\geq k\) 则有\(\geq k\)长度满足条件的方案


3.求最长奇回文字串(偶回文字串类似)

假设要在 \(T\) 串中求

设字符串 \(T^{\prime}\) 为其翻转过来的串

\(T^{\prime}\) 合并在 \(T\) 的后面(中间要加间隔符号)

\(1\) ~ \(m\) ( \(m\)\(T\) 串的长度)枚举 \(i\) ,

则求 \(i\)\(2*m+2-i\)(下标最好自己定,这是额外加了两个间隔符号的情况,即 \(T^{\prime}\) 后也有,且若为偶回文串也要另当别论)的 \(\operatorname{LCP}\),则求出以 \(i\) 为中心的最长奇回文串。

图片(待补充)