后缀数组

解决什么?

最基本的应用就是对一个字符串的所有后缀按照字典序进行排序,但对于最暴力的做法,在快速排序中(当然你用冒泡更暴力)\(cmp\) 函数暴力从头到尾枚举到第一位不同的位置 \(O(n^2logn)\),再一步优化的话就是二分来找到这一个位置 \(O(nlog^2n)\),对于 \(1e6\) 的后缀数组题的时候并不能完成,于是我们引入后缀数组的解法。

倍增法

假设我们已经通过比较每一个后缀的前 \(k\) 位得到了它们的一个排名,那是不是只需要再区分这一轮比完后排名还相同的就可以了,而接下来这一轮如果我们将比较长度扩大至 \(2*k\),那是不是后 \(k\) 位的大小也好判断,因为前一轮我们相当于把每一个长度为 \(k\) 的子串都给比较好了,所以理论上来讲就很好说通啦。

但是显然代码实现是需要动些脑筋的,于是我们定义了三个数组,\(sa_i\) 表示上一轮排序后排第 \(i\) 名的是以哪一位为开头的后缀,\(fi_i\) 表示以第 \(i\) 位为起点的后缀上一轮后排第几,\(se[i]\)\(sa\) 类似,只不过它是继承上一轮的信息,来辅助这一轮的新增长度比较的工具,也就是dalao们说的双关键字排序,上一轮的排名为第一关键字(显然优先级更高),第二关键字就是新增长度那一块,也就是起点往后数长度位后的那个位置为起点的后缀上一轮的排名,我们用 \(se_i\) 记录这第二关键字排第 \(i\) 位的起点在哪里。

比如 \(ab\),第二轮时他们的第二关键字就是 \(b\) 和一片空白,排名为2,1,所以 \(se\) 值就为 \(2,1\)(第一是第二位为起点的,第二是第一位为起点的)。

考虑每一轮如何通过上一轮的结果得到 \(se\) 数组,定义当前已经比较的长度为 \(k\)

首先,容易想到的是后 \(k\) 位肯定排在最前面,因为它们这一轮的第二关键字至少都在 \(k\) 位开外,所以就是空白,肯定字典序最小,然后再去挨个枚举 \(sa\),显然从小到大去枚举排名,如果当前排名所对应的位置在 \(k\) 之后,它就能成为 其位置\(-k\) 这一轮的关键字,所以直接将 \(sa[i]-k\) 加入 \(se\) 数组即可,由枚举顺序可知加入顺序也无误。

在考虑用 \(se\)\(fi\) 得到这一轮的 \(sa\)

易知排名始终不会大于 \(n\),因为排名始终只可能会有重叠,不会有断层,所以用桶来记录每一个排名(第一关键字)有多少人,再计算一个前缀和,那前一轮排名相同的,也就是第一关键字相同的,这一轮能被放到的位置就被局限到应到的范围内了?

然后我们再从大到小去枚举第二关键字,你就可以将先枚举到的放入它应到的范围的最后一个可以放置的位置就好了。

最后再通过得到的 \(sa\) 重新计算第一关键字。

首先你得到的 \(sa\) 序列对应的每一个后缀的已处理长度,它只会是相同或者越来越大的,所以我们需要判定的是 \(sa[i]\)\(sa[i-1]\) 对应的两后缀当前处理长度的子串是否相同,所以比较两个部分,以它们为起点的前 \(k\) 个,以它们为起点的 \(k+1\)\(2*k\) 个,两者排名都已在上一轮计算出,因为要更新第一关键字,所以先提前将第一关键字赋值给此时已经无用的第二关键字,然后比较更新就可以了。

容易发现每一层倍增中的每一个操作次数都是小于等于 \(n\) 的,因此最终复杂度 \(O(nlogn)\)


#include<bits/stdc++.h>
using namespace std;
inline void read(int &res){
	char c;
	int f=1;
	res=0;
	c=getchar();
	while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
	while(c>='0'&&c<='9')res=(res<<1)+(res<<3)+c-48,c=getchar();
	res*=f;
}
char c[1000005];
int n,m;
int fi[1000005],se[1000005],t[1000005],sa[1000005];
void qsort(){
	for(int i=0;i<=m;i++)t[i]=0;
	for(int i=1;i<=n;i++)t[fi[i]]++;
	for(int i=1;i<=m;i++)t[i]+=t[i-1];
	for(int i=n;i>=1;i--)sa[t[fi[se[i]]]--]=se[i];
}
int main()
{
	scanf("%s",c+1);
	n=strlen(c+1);
	m=128;
	for(int i=1;i<=n;i++){
		fi[i]=c[i],se[i]=i;
	}
	qsort();
	for(int k=1;k<=n;k<<=1){
		int cnt=0;
		for(int i=1;i<=k;i++)se[++cnt]=n-k+i;
		for(int i=1;i<=n;i++)if(sa[i]>k)se[++cnt]=sa[i]-k;
		qsort();
		std::swap(se,fi);
		fi[sa[1]]=cnt=1;
		for(int i=2;i<=n;i++){
			fi[sa[i]]=(se[sa[i]]==se[sa[i-1]]&&se[sa[i]+k]==se[sa[i-1]+k])?cnt:++cnt;
		}
		m=cnt;
		if(m==n)break;
	}
	for(int i=1;i<=n;i++)printf("%d ",sa[i]);
	return 0;
}

Height 数组

一个很实用的东西,定义 \(H_i\) 表示 \(lcp(sa[i-1],sa[i])\)(这里的 \(sa\) 直接表示对应后缀字符串,lcp,指最长公共前缀)。而它的用法就是对于两个后缀,例如 \(i\)\(j\),它们的排名为 \(k,l(k<l)\),它们的 lcp 就是 \(min(H[k+1],H[k+2]……H[l]\)。用 rmq \(O(n)\) 处理,\(O(1)\) 就能求解,但我只会 \(ST\)。那怎么求 \(H\),其实可以二分 \(O(nlogn)\) 求完,但万一你就要卡那个常呢,我们有一个性质,原串中一个后缀对应的 \(H\),也就是 \(H[fi[i]]\),它一定满足 \(H[fi[i]] \ge H[fi[i-1]]-1\),为啥?因为你前面那个和别人匹配了 \(k\) 位,去掉它自己那一位,不就相当于你跟别人别人匹配了 \(k-1\) 位吗,所以 \(O(n)\) 就可以求得所有 \(H\) 数组。


for(int i=1;i<=n;i++){
	int k=max(0,h[fi[i-1]]-1);
	while(c[i+k]==c[sa[fi[i]-1]+k])++k;
	h[fi[i]]=k;
}

广义SA

SA就是后缀数组,加个 \(M\) 就是自动机,但先鸽掉。

用于求两个字符串的最长公共子串,首先给一个性质,对于 \(H\) 数组,它在最终得到长为 \(k\) 的一段中值若皆大于 \(x\),则它们的一段都有一个长为 \(x\) 的公共前缀,放到原串就代表,这一公共前缀可重叠地在原串中出现了 \(k\) 次,而且如果继续在 \(H\) 中向两边扩展,一旦到达某个小于 \(x\) 的位置,则那个方向就不再有同样的公共前缀了。

所以在求两个字符串的公共子串时,我们将它们连接起来,在它们之间加一个不会出现的字符,如 #。按流程求出 \(H\) 数组,在进行二分答案,若能找到一段连续的 \(H\) 数组满足其值皆大于等于 \(mid\),且包含前后两串所对应的后缀起始点,则说明 \(mid\) 可行。

例题SP1811

就是求上面这问题,不能再板了。


#include<bits/stdc++.h>
using namespace std;
int n,m,l1,l2;
char c[500005];
char a[250005];
char b[250005];
int se[500005],fi[500005],h[500005],sa[500005];
int t[500005];
void qsort(){
	for(int i=0;i<=m;i++)t[i]=0;
	for(int i=1;i<=n;i++)t[fi[i]]++;
	for(int i=1;i<=m;i++)t[i]+=t[i-1];
	for(int i=n;i>=1;i--)sa[t[fi[se[i]]]--]=se[i];
}
int lg[500005];
int mn[500005][21];
inline void pre(){
	for(int i=1;i<=n;i++)mn[i][0]=h[i];
	for(int i=1;i<=20;i++){
		for(int j=1;j+(1<<i)-1<=n;j++){
			mn[j][i]=min(mn[j][i-1],mn[j+(1<<(i-1))][i-1]);
		}
	}
}
inline int query(int l,int r){
	int k=lg[r-l+1];
	return  min(mn[l][k],mn[r-(1<<k)+1][k]);
}
int pd(int x){
	if(x<=l1)return 1;
	return 2;
}
int check(int mid){
	for(int i=1;i<=n;i++){
		if(h[i]>=mid&&pd(sa[i-1])!=pd(sa[i]))return 1;
	}
	return 0;
}
int main()
{
	scanf("%s",a+1);
	l1=strlen(a+1);
	scanf("%s",b+1);
	l2=strlen(b+1);
	for(int i=1;i<=l1;i++)c[++n]=a[i];
	c[++n]='#';
	m=128;
	for(int i=1;i<=l2;i++)c[++n]=b[i];
	for(int i=1;i<=n;i++){
		fi[i]=c[i],se[i]=i;
	}
	qsort();
	for(int k=1;k<=n;k<<=1){
		int cnt=0;
		for(int i=1;i<=k;i++){
			se[++cnt]=n-k+i;
		}
		for(int i=1;i<=n;i++){
			if(sa[i]>k)se[++cnt]=sa[i]-k;
		}
		qsort();
		std::swap(se,fi);
		fi[sa[1]]=cnt=1;
		for(int i=2;i<=n;i++){
			fi[sa[i]]=(se[sa[i]]==se[sa[i-1]]&&se[sa[i]+k]==se[sa[i-1]+k])?cnt:++cnt;
		}
		m=cnt;
		if(m==n)break;
	}
	for(int i=1;i<=n;i++){
		int k=max(0,h[fi[i-1]]-1);
		while(c[i+k]==c[sa[fi[i]-1]+k])++k;
		h[fi[i]]=k;
	}
	lg[0]=-1;
	for(int i=1;i<=n;i++){
		lg[i]=lg[i>>1]+1;
	}
	pre();
	int l=1,r=n,ans=0;
	while(l<=r){
		int mid=(l+r)>>1;
		if(check(mid)){
			ans=mid;
			l=mid+1;
		}
		else r=mid-1;
	}
	printf("%d",ans);
}


posted on 2021-12-07 20:01  漠寒·  阅读(35)  评论(0编辑  收藏  举报