后缀数组

\(sa\)\(rk\) 数组

定义
\(sa_i\) 表示将所有后缀按字典序排序后第 \(i\) 小的后缀以 \(sa_i\) 开头, \(rk_i\) 表示以 \(i\) 开头的后缀的排名。
有性质 \(sa_{rk_i}=rk_{sa_i}=i\)
求法
用倍增优化暴力做法。
\(rk_{w,i}\) 表示以 \(i\) 开头的长度为 \(w\) 的串在所有长度为 \(w\) 的串中排名为多少。
那么,以 \(rk_{w,i}\) 为第一关键字, \(rk_{w,i+w}\) 为第二关键字排序,即可求出 \(rk_{2w}\)
把字符串中每个字符排序,得到 \(rk_1\) 后即可推出 \(rk\) 数组。
用 sort 排序,复杂度为 \(O(n \log^2 n)\)
因为这个排序的值域为 \(O(n)\) ,考虑用基数排序代替 sort 的部分,复杂度为 \(O(n \log n)\)
但是,第二关键字的排序其实并不需要计数排序。只需把空串放在前面,其它串按原顺序排好即可。
这里就放一份用 sort 实现后缀排序的代码,比较清晰,方便理解。把各种排序丢进去之后太乱了。

#include<algorithm>
#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
const int N=2e6+10;
char a[N];
int n,w,sa[N],rk[N],RK[N];
int MAX(int x,int y)
{
	return x>y?x:y;
}
bool cmp(int x,int y)
{
	if(rk[x]!=rk[y]) return rk[x]<rk[y];
	return rk[x+w]<rk[y+w];
}
int main()
{
	scanf("%s",a+1);
	n=strlen(a+1);
	int m=MAX(n,300);
	for(int i=1;i<=n;i++) sa[i]=i,rk[i]=a[i];
	//rk数组在代码中其实只关心大小关系,而并不关心具体的值 
        //进入循环后立刻就要排序,而且排序方法也和 sa 数组无关,所以 sa 数组的初值只要赋为 1~n 即可
	for(w=1;w<n;w<<=1)
	{
		sort(sa+1,sa+n+1,cmp);
		memcpy(RK,rk,sizeof(rk));
		for(int t=0,i=1;i<=n;i++)
			rk[sa[i]]=(RK[sa[i]]==RK[sa[i-1]]&&RK[sa[i]+w]==RK[sa[i-1]+w])?t:++t;
	}
	for(int i=1;i<=n;i++) printf("%d ",sa[i]);
	return 0;
}

应用
1
把字符串复制一遍,后缀排序即可。

#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
const int N=2e6+10;
char a[N];
int n,sa[N],rk[N],RK[N],cnt[N],id[N],p[N];
int MAX(int x,int y)
{
	return x>y?x:y;
}
int main()
{
	scanf("%s",a+1);
	n=strlen(a+1);
	for(int i=1;i<=n;i++) a[i+n]=a[i];n*=2;
	int m=MAX(n,300);
	for(int i=1;i<=n;i++) cnt[rk[i]=a[i]]++;
	for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
	for(int i=n;i>=1;i--) sa[cnt[rk[i]]--]=i;
	for(int w=1;w<n;w<<=1)
	{
		int sum=0;
		for(int i=n;i>n-w;i--) id[++sum]=i;
		for(int i=1;i<=n;i++)
			if(sa[i]>w) id[++sum]=sa[i]-w;
		memset(cnt,0,sizeof(cnt));
		for(int i=1;i<=n;i++) cnt[p[i]=rk[id[i]]]++;
		for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
		for(int i=n;i>=1;i--) sa[cnt[p[i]]--]=id[i];
		memcpy(RK,rk,sizeof(rk));
		for(int t=0,i=1;i<=n;i++,m=t)
			rk[sa[i]]=(RK[sa[i]]==RK[sa[i-1]]&&RK[sa[i]+w]==RK[sa[i-1]+w])?t:++t;
	}
	for(int i=1;i<=n;i++)
		if(sa[i]<=n/2) putchar(a[sa[i]+n/2-1]);
	return 0;
}

2
先考虑暴力的做法。
首先,当首尾字符不同时,显然可以贪心选。
当首尾字符相同时,则把当前剩下的串和它的反串的字典序进行比较。
考虑如何优化这一做法。
把原串的反串接在原串后面,把两个串之间用一个字典序极小的字符隔开。
求出这个串的后缀数组,比较时直接比较 \(rk\) 即可。

#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
const int N=2e6+10;
char a[N],ans[N];
int n,sa[N],rk[N],RK[N],cnt[N],id[N],p[N];
int MAX(int x,int y)
{
	return x>y?x:y;
}
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++) getchar(),a[i]=getchar();
	a[n+1]='#';
	for(int i=1,j=n;i<=n;i++,j--) a[n+1+i]=a[j];
	n*=2,n++;
	int m=MAX(n,300);
	for(int i=1;i<=n;i++) cnt[rk[i]=a[i]]++;
	for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
	for(int i=n;i>=1;i--) sa[cnt[rk[i]]--]=i;
	for(int w=1;w<n;w<<=1)
	{
		int sum=0;
		for(int i=n;i>n-w;i--) id[++sum]=i;
		for(int i=1;i<=n;i++)
			if(sa[i]>w) id[++sum]=sa[i]-w;
		memset(cnt,0,sizeof(cnt));
		for(int i=1;i<=n;i++) cnt[p[i]=rk[id[i]]]++;
		for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
		for(int i=n;i>=1;i--) sa[cnt[p[i]]--]=id[i];
		memcpy(RK,rk,sizeof(rk));
		for(int t=0,i=1;i<=n;i++,m=t)
			rk[sa[i]]=(RK[sa[i]]==RK[sa[i-1]]&&RK[sa[i]+w]==RK[sa[i-1]+w])?t:++t;
	}
	n--,n/=2;
	int l=1,r=n,res=0;
	while(l<r)
	{
		if(a[l]<a[r]) ans[++res]=a[l],l++;
		else if(a[r]<a[l]) ans[++res]=a[r],r--;
		else
		{
			if(rk[l]<rk[n*2+2-r]) ans[++res]=a[l],l++;
			else ans[++res]=a[r],r--;
		}
	}
	ans[++res]=a[l];
	for(int i=1;i<=res;i++)
	{
		putchar(ans[i]);
		if(i%80==0) puts("");
	}
	return 0;
}

3
在线地在主串 \(T\) 中寻找模式串 \(S\)
发现若 \(S\)\(T\) 中出现,\(S\) 一定是 \(T\) 某个后缀的前缀。
求出后缀数组,在求的过程中我们已经将后缀排序了,
在排序用的数组中二分,判断时暴力即可。
复杂度 \(O(|S| \log |T|)\)
若出现了很多次,发现每次出现时,我们要寻找的后缀在排序后一定是连续的,所以再二分一次即可。

\(height\) 数组

下面以 \(lcp(i,j)\) 表示后缀 \(i\) 和后缀 \(j\) 的最长公共前缀的长度。
定义
\(height_i=lcp(sa_i,sa_{i-1})\)
求法
\(height_{rk_i} \geq height_{rk_{i-1}}-1\)
根据这个式子,按照 \(rk\) 的顺序暴力求,容易证明复杂度是 \(O(n)\) 的。

for(int i=1,t=0;i<=n;i++)
{
	if(t) t--;
	while(a[i+t]==a[sa[rk[i]-1]+t]) t++;
	ht[rk[i]]=t;
}

应用
1
求任意后缀的 \(lcp\)
\(lcp(x,y)=min\{height_k|rk_x <k \leq rk_y\}\)
2
求不同子串的数目。
子串就是后缀的前缀。
考虑容斥一下,用串的总数减去重复的串的个数。
按排序得到的顺序枚举后缀,发现每次重复的子串的数量即为在与前一个后缀的 \(lcp\) 里的前缀的数量。
所以,答案即为 \(\frac{n(n+1)}{2}-\sum \limits_{i=2}^n height_i\)
3
出现至少 \(k\) 次可以转化为在排序后的后缀中有至少连续 \(k\) 个后缀的 \(lcp\) 是这个串。
所以,只需求出每相邻 \(k-1\)\(height\) 的最小值,在求出它们的最大值即可。
用单调队列实现。

#include<iostream>
#include<cstring>
#include<cstdio>
#include<deque>
using namespace std;
const int N=2e6+10;
int sa[N],rk[N],RK[N],cnt[N],id[N],p[N];
int n,k,a[N],ht[N],ans;
struct node
{
	int id,x;
};
deque <node> q;
int MAX(int x,int y)
{
	return x>y?x:y;
}
int main()
{
	scanf("%d%d",&n,&k);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	int m=MAX(n,300);
	for(int i=1;i<=n;i++) cnt[rk[i]=a[i]]++;
	for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
	for(int i=n;i>=1;i--) sa[cnt[rk[i]]--]=i;
	for(int w=1;w<n;w<<=1)
	{
		int sum=0;
		for(int i=n;i>n-w;i--) id[++sum]=i;
		for(int i=1;i<=n;i++)
			if(sa[i]>w) id[++sum]=sa[i]-w;
		memset(cnt,0,sizeof(cnt));
		for(int i=1;i<=n;i++) cnt[p[i]=rk[id[i]]]++;
		for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
		for(int i=n;i>=1;i--) sa[cnt[p[i]]--]=id[i];
		memcpy(RK,rk,sizeof(rk));
		for(int t=0,i=1;i<=n;i++,m=t)
			rk[sa[i]]=(RK[sa[i]]==RK[sa[i-1]]&&RK[sa[i]+w]==RK[sa[i-1]+w])?t:++t;
	}
	for(int i=1,t=0;i<=n;i++)
	{
		if(t) t--;
		while(a[i+t]==a[sa[rk[i]-1]+t]) t++;
		ht[rk[i]]=t;
	}
	for(int i=2;i<=n;i++) 
	{
		while(q.size()&&ht[i]<=q.back().x) q.pop_back();
		q.push_back((node){i,ht[i]});
		while(q.front().id<=i-k+1) q.pop_front();
		if(i>=k) ans=MAX(ans,q.front().x);
	}
	printf("%d",ans);
	return 0;
}

4
给出一个文本串,问是否有字符串在文本串中至少不重叠地出现了两次。
二分字符串的长度 \(x\) ,容易发现这一定是单调的,所以可以二分。
\(height\) 数组中找出所有连续 \(lcp\) 大于等于 \(x\) 的段,
对于每段找出后缀编号最小和最大的后缀,判断是否合法即可。
5
可以按照 \(height\) 数组大小的顺序合并答案,这部分用并查集维护。
因为最大的乘积可能是由最小值相乘,或由最大值相乘得到,所以要维护下最小值、最大值。
方案数即为合并时的两子树大小相乘。
发现若两个串是 \(r\) 相似的,则它们一定也是 \(1\) 相似,\(2\) 相似, \(\cdots\)\(r-1\) 相似的。
所以最后要做前缀和。

#include<algorithm>
#include<iostream>
#include<cstring>
#include<cstdio>
#define int long long
using namespace std;
const int N=2e6+10;
const int inf=1e18;
char a[N];
int sa[N],rk[N],RK[N],cnt[N],id[N],p[N],val[N];
int n,ht[N],fa[N],sum[N],ans[N],mx[N],mn[N],sz[N];
int MIN(int x,int y)
{
	return x<y?x:y;
}
int MAX(int x,int y)
{
	return x>y?x:y;
}
bool cmp(int x,int y)
{
	return ht[x]>ht[y];
}
int find(int x)
{
	if(x==fa[x]) return x;
	return fa[x]=find(fa[x]);
}
void merge(int x,int y)
{
	int fx=find(x),fy=find(y);
	sum[ht[x]]+=sz[fx]*sz[fy];
	ans[ht[x]]=MAX(ans[ht[x]],MAX(mx[fx]*mx[fy],mn[fx]*mn[fy]));
	mx[fx]=MAX(mx[fx],mx[fy]);
	mn[fx]=MIN(mn[fx],mn[fy]);
	fa[fy]=fx,sz[fx]+=sz[fy];
}
signed main()
{
	scanf("%lld",&n);
	scanf("%s",a+1);
	for(int i=1;i<=n;i++) scanf("%lld",&val[i]);
	int m=MAX(n,300);
	for(int i=1;i<=n;i++) cnt[rk[i]=a[i]]++;
	for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
	for(int i=n;i>=1;i--) sa[cnt[rk[i]]--]=i;
	for(int w=1;w<n;w<<=1)
	{
		int sum=0;
		for(int i=n;i>n-w;i--) id[++sum]=i;
		for(int i=1;i<=n;i++)
			if(sa[i]>w) id[++sum]=sa[i]-w;
		memset(cnt,0,sizeof(cnt));
		for(int i=1;i<=n;i++) cnt[p[i]=rk[id[i]]]++;
		for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
		for(int i=n;i>=1;i--) sa[cnt[p[i]]--]=id[i];
		memcpy(RK,rk,sizeof(rk));
		for(int t=0,i=1;i<=n;i++,m=t)
			rk[sa[i]]=(RK[sa[i]]==RK[sa[i-1]]&&RK[sa[i]+w]==RK[sa[i-1]+w])?t:++t;
	}
	for(int i=1,t=0;i<=n;i++)
	{
		if(t) t--;
		while(a[i+t]==a[sa[rk[i]-1]+t]) t++;
		ht[rk[i]]=t;
	}
	for(int i=1;i<=n;i++)
		id[i]=i,fa[i]=i,ans[i]=-inf,sz[i]=1,mx[i]=val[sa[i]],mn[i]=val[sa[i]];
	sort(id+1,id+n+1,cmp);
	for(int i=1;i<=n;i++)
		if(find(id[i])!=find(id[i]-1)) merge(id[i],id[i]-1);
	for(int i=n-2;i>=0;i--)
		sum[i]+=sum[i+1],ans[i]=MAX(ans[i],ans[i+1]);
	for(int i=0;i<n;i++)
	{
		if(sum[i]==0) puts("0 0");
		else printf("%lld %lld\n",sum[i],ans[i]);
	}
	return 0;
}

6
这个式子前面的部分可以直接算,所以就是在求后缀两两之间的 \(lcp\) 之和。
考虑 \(lcp\) 的求法,可以把求 \(lcp\) 之和转化为求 \(height\) 数组每段区间的区间最小值之和。
这就是单调栈经典问题了。

#include<iostream>
#include<cstring>
#include<cstdio>
#define int long long
using namespace std;
const int N=2e6+10;
const int inf=1e18;
char a[N],b[N];
int sa[N],rk[N],RK[N],cnt[N],id[N],p[N];
int n,ht[N],ans,top,s[N],l[N],r[N];
int MAX(int x,int y)
{
	return x>y?x:y;
}
signed main()
{
	scanf("%s",a+1);
	n=strlen(a+1);
	int m=MAX(n,300);
	for(int i=1;i<=n;i++) cnt[rk[i]=a[i]]++;
	for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
	for(int i=n;i>=1;i--) sa[cnt[rk[i]]--]=i;
	for(int w=1;w<n;w<<=1)
	{
		int sum=0;
		for(int i=n;i>n-w;i--) id[++sum]=i;
		for(int i=1;i<=n;i++)
			if(sa[i]>w) id[++sum]=sa[i]-w;
		memset(cnt,0,sizeof(cnt));
		for(int i=1;i<=n;i++) cnt[p[i]=rk[id[i]]]++;
		for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
		for(int i=n;i>=1;i--) sa[cnt[p[i]]--]=id[i];
		memcpy(RK,rk,sizeof(rk));
		for(int t=0,i=1;i<=n;i++,m=t)
			rk[sa[i]]=(RK[sa[i]]==RK[sa[i-1]]&&RK[sa[i]+w]==RK[sa[i-1]+w])?t:++t;
	}
	for(int i=1,t=0;i<=n;i++)
	{
		if(t) t--;
		while(a[i+t]==a[sa[rk[i]-1]+t]) t++;
		ht[rk[i]]=t;
	}
	ans=(n-1)*(n+1)*n/2;
	ht[0]=-inf,ht[n+1]=-inf;
	s[++top]=0;
	for(int i=1;i<=n;i++)
	{
		while(top&&ht[i]<=ht[s[top]]) top--;
		l[i]=s[top],s[++top]=i;
	}
	s[top=1]=n+1;
	for(int i=n;i>=1;i--)
	{
		while(top&&ht[i]<ht[s[top]]) top--;
		r[i]=s[top],s[++top]=i;
	}
	for(int i=1;i<=n;i++) ans-=2*(i-l[i])*(r[i]-i)*ht[i];
	printf("%lld",ans);
	return 0;
}
posted @ 2021-01-14 19:08  BBD186587  阅读(98)  评论(0编辑  收藏  举报