后缀数组小结

后缀数组

概念

后缀 \(suffix\) 是指字符串中从某个位置开始,到最后一个位置,组成的字符串。
而后缀数组就是将上述所有的后缀,按照字典序从小到大的顺序,重新排列,并用它们各自的起始下标来表示后缀字符串,组成的整数数组。

构造

快排的时间复杂度为 \(O(nlogn)\) ,而字符串比较需要 \(O(n)\) ,所以直接暴力的时间复杂度为 \(O(n^2logn)\)
用倍增的方法可以优化到 \(O(nlogn)\)
我们考虑每次排序后缀的长度为 \(len\) 的前缀,每次 \(len\) 乘上 \(2\)
设前一次排序后第 \(i\) 个位置是第 \(x_i\) 大,那么,这次排序第 \(i\) 个位置向后的长度为 \(len\) 的字符串可以看成 \(x_i,x_{i+len}\) 两个值拼在一起。
那么每次排序就可以看成双关键字排序,时间复杂度为 \(O(nlog^2n)\)
瓶颈在于排序,我们把排序改成基数排序,这样就做到了 \(O(nlogn)\)

Code

	n=a.size(),a=' '+a,m=130;
	for(int i=1;i<=n;i++)x[i]=a[i],t[x[i]]++;
	for(int i=1;i<=m;i++)t[i]+=t[i-1];
	for(int i=1;i<=n;i++)sa[t[x[i]]--]=i;//预处理
	for(int p=1;p<n;p<<=1)
	{
		num=0;
		for(int j=n-p+1;j<=n;j++)y[++num]=j;//双关键字基数排序,先将第二关键字排好序,不存在第二关键字的排最前面
		for(int j=1;j<=n;j++)if(sa[j]>p)y[++num]=sa[j]-p;//若sa[j]比p大,说明sa[j]可以作为sa[j]-p的第二关键字
		memset(t,0,sizeof(t));
		for(int j=1;j<=n;j++)t[x[j]]++;
		for(int j=1;j<=m;j++)t[j]+=t[j-1];
		for(int j=n;j>=1;j--)sa[t[x[y[j]]]--]=y[j];//按第一关键字排序
		for(int j=1;j<=n;j++)y[j]=x[j];
		m=1,x[sa[1]]=1;
		for(int j=2;j<=n;j++)x[sa[j]]=(y[sa[j]]==y[sa[j-1]]&&y[sa[j]+p]==y[sa[j-1]+p])?m:(++m);//处理出每一位是第几大
		if(m==n)break;
	}

性质

sa,rank,height,h数组

\(sa_i\) 代表的是第 \(i\) 小的后缀以 \(sa_i\) 开头,而 \(rank_i\) 代表的是第 \(i\) 位开头的后缀是第 \(rank_i\) 大。
\(height_i\) 指的是 \(sa_i\)\(sa_{i-1}\)\(LCP\)\(height_1=0\) ,而 \(h_i\) 指的是 \(i\)\(i\) 的最大的比他小的后缀的 \(LCP\) ,即 \(h_{sa_i}=height_i,height_{rank_i}=h_i\)

任意两个后缀的LCP

有一个定理:对于 \(i \le j \le k\) ,有 \(min(LCP(sa_i,sa_j),LCP(sa_j,sa_k))=LCP(sa_i,sa_k)\)
证明可用反证法证。
那这样我们就可以套 \(RMQ\) 了。

height数组的求解

首先有一个定理 \(h_i \ge h_{i-1}-1\)
证明可以结合上边的结论来证。
有了这个定理,我们就可以一位一位的求出 \(h_i\) ,推出 \(height_i\) 来。

后缀数组模板

	n=a.size(),a=' '+a,m=130;
	for(int i=1;i<=n;i++)x[i]=a[i],t[x[i]]++;
	for(int i=1;i<=m;i++)t[i]+=t[i-1];
	for(int i=1;i<=n;i++)sa[t[x[i]]--]=i;
	for(int p=1;p<n;p<<=1)
	{
		num=0;
		for(int j=n-p+1;j<=n;j++)y[++num]=j;
		for(int j=1;j<=n;j++)if(sa[j]>p)y[++num]=sa[j]-p;
		memset(t,0,sizeof(t));
		for(int j=1;j<=n;j++)t[x[j]]++;
		for(int j=1;j<=m;j++)t[j]+=t[j-1];
		for(int j=n;j>=1;j--)sa[t[x[y[j]]]--]=y[j];
		for(int j=1;j<=n;j++)y[j]=x[j];
		m=1,x[sa[1]]=1;
		for(int j=2;j<=n;j++)x[sa[j]]=(y[sa[j]]==y[sa[j-1]]&&y[sa[j]+p]==y[sa[j-1]+p])?m:(++m);
		if(m==n)break;
	}
	for(int i=1;i<=n;i++)rk[sa[i]]=i;
	for(int i=1;i<=n;i++)
	{
		if(rk[i]==1)continue;
		h[i]=(h[i-1]>=1)?(h[i-1]-1):0;
		while(a[i+h[i]]==a[sa[rk[i]-1]+h[i]])h[i]++;
		height[rk[i]]=h[i]; 
	}

题目

【Luogu P3809】【模板】后缀排序

直接求,套模板即可。

【Luogu P2408】 不同子串个数

题目描述

求出一个字符串的不同子串个数, \(|S| \le 10^5\)

解题思路

一个字符串的子串可以看成该字符串某个后缀的前缀,我们只需要求出有多个后缀的前缀是和前一个重复的即可。

Code

#include<bits/stdc++.h>
using namespace std;
int n,sa[100005],x[300005],y[300005],m,t[100005],num,rk[100005];
long long height[100005],h[100005];
string a;
int main()
{
	scanf("%d",&n);
	cin>>a,m=130,a=' '+a;
	for(int i=1;i<=n;i++)x[i]=a[i],t[x[i]]++;
	for(int i=1;i<=m;i++)t[i]+=t[i-1];
	for(int i=1;i<=n;i++)sa[t[x[i]]--]=i;
	for(int p=1;p<n;p<<=1)
	{
		num=0;
		for(int j=n-p+1;j<=n;j++)y[++num]=j;
		for(int j=1;j<=n;j++)if(sa[j]>p)y[++num]=sa[j]-p;
		memset(t,0,sizeof(t));
		for(int j=1;j<=n;j++)t[x[j]]++;
		for(int j=1;j<=m;j++)t[j]+=t[j-1];
		for(int j=n;j>=1;j--)sa[t[x[y[j]]]--]=y[j];
		swap(x,y),m=1,x[sa[1]]=1;
		for(int j=2;j<=n;j++)x[sa[j]]=(y[sa[j]]==y[sa[j-1]]&&y[sa[j]+p]==y[sa[j-1]+p])?m:(++m); 
	}
	for(int i=1;i<=n;i++)rk[sa[i]]=i;
	for(int i=1;i<=n;i++)
	{
		if(rk[i]==1)continue;
		h[i]=(h[i-1]>=1)?(h[i-1]-1):0;
		while(a[i+h[i]]==a[sa[rk[i]-1]+h[i]])h[i]++;
		height[rk[i]]=h[i];
	}
	long long s=0;
	for(int i=1;i<=n;i++)s+=height[i]; 
	s=(long long)(n)*(n+1)/2-s;
	printf("%lld",s);
  return 0;
}

【Luogu P3181】 找相同字符

题目描述

给定两个字符串,求出在两个字符串中各取出一个子串使得这两个子串相同的方案数,\(|S_1|,|S_2| \le 2 \times 10^5\)

解题思路

首先,我们可以将两个字符串拼在一起,中间加一个未出现过的字符以分隔。
我们该字符串有多少对重复子串,然后减去原来两个字符串各自的重复字串个数之和。
求一个字符串有多少对重复字串,这个东西可以用后缀数组加单调栈解决。

Code

#include<bits/stdc++.h>
using namespace std;
int n,m,x[800005],y[800005],t[400005],sa[400005],num,height[400005],h[400005],rk[400005],pre[400005],nex[400005];
stack<int> l;
long long dijah(string a)
{
	n=a.size(),a=' '+a,m=130;
	memset(t,0,sizeof(t));
	memset(y,0,sizeof(y));
	for(int i=1;i<=n;i++)x[i]=a[i],t[x[i]]++;
	for(int i=1;i<=m;i++)t[i]+=t[i-1];
	for(int i=1;i<=n;i++)sa[t[x[i]]--]=i;
	for(int p=1;p<n;p<<=1)
	{
		num=0;
		for(int j=n-p+1;j<=n;j++)y[++num]=j;
		for(int j=1;j<=n;j++)if(sa[j]>p)y[++num]=sa[j]-p;
		memset(t,0,sizeof(t));
		for(int j=1;j<=n;j++)t[x[j]]++;
		for(int j=1;j<=m;j++)t[j]+=t[j-1];
		for(int j=n;j>=1;j--)sa[t[x[y[j]]]--]=y[j];
		for(int j=1;j<=n;j++)y[j]=x[j];
		m=1,x[sa[1]]=1;
		for(int j=2;j<=n;j++)x[sa[j]]=(y[sa[j]]==y[sa[j-1]]&&y[sa[j]+p]==y[sa[j-1]+p])?(m):(++m);
		if(m==n)break;
	}
	for(int i=1;i<=n;i++)rk[sa[i]]=i;
	long long s=0;
	for(int i=1;i<=n;i++)
	{
		if(rk[i]==1){h[i]=0;continue;}
		h[i]=(h[i-1]>=1)?(h[i-1]-1):0;
		while(a[i+h[i]]==a[sa[rk[i]-1]+h[i]])h[i]++;
		height[rk[i]]=h[i];
	}
	while(l.size()!=0)l.pop();
	for(int i=1;i<=n;i++)
	{
		while(l.size()!=0&&height[l.top()]>=height[i])l.pop();
		if(l.size()!=0)pre[i]=l.top();
		else pre[i]=0;
		l.push(i);
	}
	while(l.size()!=0)l.pop();
	for(int i=n;i>=1;i--)
	{
		while(l.size()!=0&&height[l.top()]>height[i])l.pop();
		if(l.size()!=0)nex[i]=l.top();
		else nex[i]=n+1;
		l.push(i);
	}
	for(int i=1;i<=n;i++)s+=(long long)(height[i])*(i-pre[i])*(nex[i]-i);
	return s;
}
int main()
{
	string a,b;
	cin>>a>>b;
	cout<<dijah(a+'#'+b)-dijah(a)-dijah(b)<<'\n';

  return 0;
}

【BZOJ2865】字符串识别

题目描述

给出一个字符串,求该字符串每一位包含其的最短且在原字符串中只出现过一次的子串的长度是多少。

解题思路

首先,我们可以用后缀数组求出对于每一个后缀,它不重复最短前缀的长度是多少。
那么,对于每一位,可以看成两种情况:前面的一位到该位的子串有重的与无重的。
对于两种情况,分别开一个树状数组即可。

Code

#include<bits/stdc++.h>
using namespace std;
string a;
int t[1000005],x[2000005],y[2000005],sa[2000005],rk[2000005],height[2000005],h[2000005],num,n,m,v[1000005];
int f1[2000005],f2[2000005];
int lowbit(int x)
{
	return x&(-x);
}
void dijah1(int x,int y)
{
	for(int i=x;i<=n;i+=lowbit(i))f1[i]=max(f1[i],y);
	return;
}
void dijah2(int x,int y)
{
	for(int i=x;i<=n;i+=lowbit(i))f2[i]=min(f2[i],y);
	return;
}
int gaia1(int x)
{
	int h=-1e9-5;
	while(x)h=max(h,f1[x]),x-=lowbit(x);
	return h;
}
int gaia2(int x)
{
	int h=1e9+5;
	while(x)h=min(h,f2[x]),x-=lowbit(x);
	return h;
}
int main()
{
	memset(f1,-2,sizeof(f1));
	memset(f2,1,sizeof(f2));
	cin>>a;
	m=130,n=a.size(),a=' '+a;
	for(int i=1;i<=n;i++)x[i]=a[i],t[x[i]]++;
	for(int i=1;i<=m;i++)t[i]+=t[i-1];
	for(int i=1;i<=n;i++)sa[t[x[i]]--]=i;
	for(int p=1;p<n;p<<=1)
	{
		num=0;
		for(int j=n-p+1;j<=n;j++)y[++num]=j;
		for(int j=1;j<=n;j++)if(sa[j]>p)y[++num]=sa[j]-p;
		memset(t,0,sizeof(t));
		for(int j=1;j<=n;j++)t[x[j]]++;
		for(int j=1;j<=m;j++)t[j]+=t[j-1];
		for(int j=n;j>=1;j--)sa[t[x[y[j]]]--]=y[j];
		for(int j=1;j<=n;j++)y[j]=x[j];
		m=1,x[sa[1]]=1;
		for(int j=2;j<=n;j++)x[sa[j]]=(y[sa[j]]==y[sa[j-1]]&&y[sa[j]+p]==y[sa[j-1]+p])?m:(++m);
		if(m==n)break;
	} 
	for(int i=1;i<=n;i++)rk[sa[i]]=i;
	for(int i=1;i<=n;i++)
	{
		if(rk[i]==1)continue;
		h[i]=(h[i-1]>=1)?(h[i-1]-1):0;
		while(a[i+h[i]]==a[sa[rk[i]-1]+h[i]])h[i]++;
		height[rk[i]]=h[i];
	}
	for(int i=1;i<=n;i++)v[sa[i]]=max(height[i],height[i+1])+1;
	for(int i=1;i<=n;i++)
	{
		if(i+v[i]!=n+2)dijah1(i+v[i]-1,i),dijah2((n+1)-(i+v[i]-1)+1,v[i]);
		printf("%d\n",min(i-gaia1(i)+1,gaia2((n+1)-i+1))); 
	}

  return 0;
}

【BZOJ4310】 跳蚤

题目描述

将一个字符串 \(S\) 分成 \(k\) 个字符串,让这些字符串的最大子串最小,求最大子串。

解题思路

首先,我们可以用后缀数组确定每个子串的排序后的位置,因为求最小的最大,考虑二分。
二分的是第 \(i\) 大的子串,我们检查就把会比他大的后缀提出来,排序,每次尽量往前断开,贪心检查。

Code

#include<bits/stdc++.h>
using namespace std;
struct datay
{
	long long x,y;
}b[100005];
long long k,n,m,num,x[200005],y[200005],t[100005],sa[100005],rk[100005],height[100005],h[100005],f[100005][21],lo[100005],po[25];
long long l[100005],len[100005],z;
string a;
bool cmp(datay q,datay w)
{
	return q.x<w.x;
}
long long gaia(int x,int y)
{
	if(x>y)swap(x,y);
	x++,z=lo[y-x+1];
	return min(f[x][z],f[y-po[z]+1][z]);
}
long long search(long long p)
{
	long long le=1,ri=n,s=0,mid;
	while(le<=ri)
	{
		mid=(le+ri)>>1;
		if(l[mid]<=p)s=max(s,mid),le=mid+1;
		else ri=mid-1;
	}
	return s;
}
bool check(long long p)
{
	long long p1=search(p),p2=n-len[p1]+1+(p-l[p1]),q=2*n,s=0;
//	cout<<p1<<' '<<p2<<'\n';
//	for(int i=sa[p1];i<=p2;i++)cout<<a[i];
//	cout<<'\n';
	num=0;
	if(p2!=n)b[++num].x=sa[p1],b[num].y=p2-sa[p1]+1;
	for(int i=1;i<=n;i++)
	{
		if(i==p1)continue;
		if(i>p1)b[++num].x=sa[i],b[num].y=min(gaia(i,p1),p2-sa[p1]+1);
		else if(gaia(p1,i)>=p2-sa[p1]+1)b[++num].x=sa[i],b[num].y=p2-sa[p1]+1;
	}
	sort(b+1,b+num+1,cmp);
//	for(int i=1;i<=num;i++)cout<<b[i].x<<' '<<b[i].y<<'\n';
	for(int i=num;i>=1;i--)
	{
		if(b[i].y<=0)return false;
		if(b[i].x+b[i].y-1==n)continue;
		if(b[i].x+b[i].y<=q)q=b[i].x,s++;
	}
//	cout<<s<<'\n';
	if(s<=k)return true;
	return false;
}
int main()
{
	scanf("%lld",&k),k--;
	cin>>a,n=a.size(),m=130,a=' '+a,po[0]=1,lo[0]=-1;
	for(int i=1;i<=20;i++)po[i]=po[i-1]<<1;
	for(int i=1;i<=n;i++)lo[i]=lo[i>>1]+1;
	for(int i=1;i<=n;i++)x[i]=a[i],t[x[i]]++;
	for(int i=1;i<=m;i++)t[i]+=t[i-1];
	for(int i=1;i<=n;i++)sa[t[x[i]]--]=i;
	for(int p=1;p<n;p<<=1)
	{
		num=0;
		for(int j=n-p+1;j<=n;j++)y[++num]=j;
		for(int j=1;j<=n;j++)if(sa[j]>p)y[++num]=sa[j]-p;
		memset(t,0,sizeof(t));
		for(int j=1;j<=n;j++)t[x[j]]++;
		for(int j=1;j<=m;j++)t[j]+=t[j-1];
		for(int j=n;j>=1;j--)sa[t[x[y[j]]]--]=y[j];
		for(int j=1;j<=n;j++)y[j]=x[j];
		m=1,x[sa[1]]=1;
		for(int j=2;j<=n;j++)x[sa[j]]=(y[sa[j]]==y[sa[j-1]]&&y[sa[j]+p]==y[sa[j-1]+p])?m:(++m);
		if(m==n)break;
	}
	for(int i=1;i<=n;i++)rk[sa[i]]=i;
	for(int i=1;i<=n;i++)
	{
		if(rk[i]==1)continue;
		h[i]=(h[i-1]>=1)?(h[i-1]-1):0;
		while(a[i+h[i]]==a[sa[rk[i]-1]+h[i]])h[i]++;
		f[rk[i]][0]=height[rk[i]]=h[i];
	}
	for(int i=1;po[i]<=n;i++)
	{
		for(int j=1;j+po[i]-1<=n;j++)f[j][i]=min(f[j][i-1],f[j+po[i-1]][i-1]);
	}
	l[1]=1,len[1]=n-sa[1]+1;
	for(int i=2;i<=n;i++)l[i]=l[i-1]+len[i-1],len[i]=n-(sa[i]+height[i])+1;
	long long le=1,ri=l[n]+len[n]-1,mid,s=1e13+5;
	while(le<=ri)
	{
		mid=(le+ri)>>1;
		if(check(mid))ri=mid-1,s=min(s,mid);
		else le=mid+1;
	}
	long long p1=search(s),p2=n-l[p1]+1+(s-len[p1]);
	for(int i=sa[p1];i<=p2;i++)cout<<a[i];
		

  return 0;
}

【Luogu P1117】 优秀的拆分

题目描述

求一个字符串中有多少个呈 \(AABB\) 形式的子串。

解题思路

考虑将 \(AABB\) 拆开计算,求 \(AA\) 形式的子串即可。
用后缀数组处理,暴力 \(O(n^2)\) 能拿 95pts,考虑优化。
我们可以枚举长度 \(len\),在字符串上设置 \(n/len\) 个关键点,那么一个满足要求的字符串必经过两个关键点。
设枚举的关键点为 \(i,i+len\) ,求出两个关键点后缀的最长公共前缀与前缀的最长公共后缀,设为 \(x,y\)
\(x+y<len\) 那必定不满足要求。
否则,可以发现重复的区域是满足要求的。
用差分来修改一段区间即可。

Code

#include<bits/stdc++.h>
using namespace std;
int n,m,num,t[30005],x[60005],y[60005],sa[30005],rk[30005],height[30005],h[30005],po[21],lo[100005],rk1[30005],rk2[30005],f1[30005][21],f2[30005][21],z;
long long f[60005],g[60005];
void dijah(string a)
{
	memset(t,0,sizeof(t));
	memset(y,0,sizeof(y));
	memset(h,0,sizeof(h));
	n=a.size(),a=' '+a,m=130;
	for(int i=1;i<=n;i++)x[i]=a[i],t[x[i]]++;
	for(int i=1;i<=m;i++)t[i]+=t[i-1];
	for(int i=1;i<=n;i++)sa[t[x[i]]--]=i;
	for(int p=1;p<n;p<<=1)
	{
		num=0;
		for(int j=n-p+1;j<=n;j++)y[++num]=j;
		for(int j=1;j<=n;j++)if(sa[j]>p)y[++num]=sa[j]-p;
		memset(t,0,sizeof(t));
		for(int j=1;j<=n;j++)t[x[j]]++;
		for(int j=1;j<=m;j++)t[j]+=t[j-1];
		for(int j=n;j>=1;j--)sa[t[x[y[j]]]--]=y[j];
		for(int j=1;j<=n;j++)y[j]=x[j];
		m=1,x[sa[1]]=1;
		for(int j=2;j<=n;j++)x[sa[j]]=(y[sa[j]]==y[sa[j-1]]&&y[sa[j]+p]==y[sa[j-1]+p])?m:(++m);
		if(m==n)break;
	}
	for(int i=1;i<=n;i++)rk[sa[i]]=i;
	for(int i=1;i<=n;i++)
	{
		if(rk[i]==1)continue;
		h[i]=(h[i-1]>=1)?(h[i-1]-1):0;
		while(a[i+h[i]]==a[sa[rk[i]-1]+h[i]])h[i]++;
		height[rk[i]]=h[i]; 
	}
	return;
}
string re(string a)
{
	string c="";
	for(int i=0;i<a.size();i++)c=a[i]+c;
	return c;
}
int gaia1(int x,int y)
{
	if(x>y)swap(x,y);
	x++,z=lo[y-x+1];
	return min(f1[x][z],f1[y-po[z]+1][z]);
}
int gaia2(int x,int y)
{
	if(x>y)swap(x,y);
	x++,z=lo[y-x+1];
	return min(f2[x][z],f2[y-po[z]+1][z]);
}
void poi()
{
	memset(f,0,sizeof(f)),memset(g,0,sizeof(g));
	string a;
	cin>>a;
	dijah(a);
	for(int i=1;i<=n;i++)rk1[i]=rk[i],f1[i][0]=height[i];
	a=re(a),dijah(a),a=re(a),a=' '+a;
	for(int i=1;i<=n;i++)rk2[i]=rk[n-i+1];
	for(int i=1;i<=n;i++)f2[rk2[i]][0]=h[n-i+1];
	for(int i=1;po[i]<=n;i++)
	{
		for(int j=1;j+po[i]-1<=n;j++)f1[j][i]=min(f1[j][i-1],f1[j+po[i-1]][i-1]),f2[j][i]=min(f2[j][i-1],f2[j+po[i-1]][i-1]);
	}
	int q,w,e;
	for(int i=2;i<=n/2;i++)
	{
		if(n%i==0&&gaia1(rk1[n-2*i+1],rk1[n-i+1])>=i)f[n-2*i+1]++,f[n-2*i+2]--,g[n]++,g[n+1]--;
		for(int j=i;j+i<n;j+=i)
		{
			q=gaia2(rk2[j],rk2[j+i]),w=gaia1(rk1[j+1],rk1[j+i+1]),q=min(q,i),w=min(w,i-1);
			if(q+w<i)continue;
			e=q+w-i+1,f[j-q+1]++,f[j-q+e+1]--;
			g[j+i+w+1]--,g[j+i+w-e+1]++;
		}
	}
	for(int i=1;i<n;i++)if(a[i]==a[i+1])f[i]++,f[i+1]--,g[i+1]++,g[i+2]--;
	for(int i=1;i<=n;i++)f[i]+=f[i-1],g[i]+=g[i-1];
	long long s=0;
	for(int i=2;i<=n;i++)s+=g[i-1]*f[i];
	printf("%lld\n",s);
	return;
}
int main()
{
	int qwe;
	scanf("%d",&qwe);
	po[0]=1,lo[0]=-1;
	for(int i=1;i<=20;i++)po[i]=po[i-1]<<1;
	for(int i=1;i<=30000;i++)lo[i]=lo[i>>1]+1;
	for(int i=1;i<=qwe;i++)poi();
	
  return 0;
}

posted @ 2024-04-11 09:15  dijah  阅读(12)  评论(0编辑  收藏  举报