后缀数组

后缀数组简介

什么是后缀数组

后缀数组 S A [ ] SA[] SA[]保存的是 1 ∼ n 1\sim n 1n的一个排列,其每个位置的元素代表将整个字符串的 n n n个后缀排序后第 i i i小的后缀的首字母的下标。

如何求后缀数组

在求之前,先记住几个变量所代表的含义:

  • s a [ i ] sa[i] sa[i]:后缀数组,代表第 i i i小的后缀的首字母下标。
  • r k [ i ] rk[i] rk[i]:名次数组,代表首字母下标为 i i i的后缀的名次。

上面两个数组可以理解为是“逆运算”的关系,所以有 s a [ r k [ i ] ] = r k [ s a [ i ] ] = i sa[rk[i]]=rk[sa[i]]=i sa[rk[i]]=rk[sa[i]]=i

  • h e i g h t [ i ] height[i] height[i]:高度数组,代表后缀数组中相邻的两个后缀的 L C P LCP LCP(最长公共前缀)

现在问题回到如何求上。
对于 s a [ ] sa[] sa[],考虑倍增,可以利用相邻的两个 2 i 2^i 2i长度的串拼起来来实现 2 i + 1 2^{i+1} 2i+1长度的串的大小比较。
因为有大量的重合部分,采用基数排序。

int n=strlen(s+1),m=10000,p=0;
for (int i=1;i<=n;i++) wb[x[i]=s[i]]++;
for (int i=1;i<=m;i++) wb[i]+=wb[i-1];
for (int i=n;i>=1;i--) sa[wb[x[i]]--]=i;
for (int j=1;p<n;j*=2,m=p)
{
	p=0;
	for (int i=n-j+1;i<=n;i++) y[++p]=i;
	for (int i=1;i<=n;i++) if (sa[i]>j) y[++p]=sa[i]-j;
	for (int i=1;i<=m;i++) wb[i]=0;
	for (int i=1;i<=n;i++) wb[x[y[i]]]++;
	for (int i=1;i<=m;i++) wb[i]+=wb[i-1];
	for (int i=n;i>=1;i--) sa[wb[x[y[i]]]--]=y[i];
	for (int i=1;i<=n;i++) swap(x[i],y[i]);
	p=1,x[sa[1]]=1;
	for (int i=2;i<=n;i++)
	{
		if (y[sa[i]]==y[sa[i-1]] && y[sa[i]+j]==y[sa[i-1]+j]) x[sa[i]]=p;
		else x[sa[i]]=++p;
	}
} 

解释其中几点:

  • 倍增的终止条件为 p &lt; n p&lt;n p<n:这里 p p p是大小严格不相同的串的个数,显然当我们给所有数都分出了一个大小之后会有 n n n个不相同的串,故为终止条件,结束倍增。
  • x [ ] x[] x[]的含义:基数排序的第一关键字。相当于保存了某一位的排名,因为我们只需要一个相对的大小来比较,故一开始可以采用 A S C L L ASCLL ASCLL码,后面也只需要通过递增排名来决定。
  • y [ ] y[] y[]的含义:基数排序的第二关键字。

求得 s a [ ] sa[] sa[]后, r k [ s a [ i ] ] = i rk[sa[i]]=i rk[sa[i]]=i求得 r k [ ] rk[] rk[]

for (int i=1;i<=n;i++)
	rk[sa[i]]=i;

考虑如何求 h e i g h t [ ] height[] height[]
h e i g h t [ ] height[] height[]数组有一个性质: h e i g h t [ r k [ i ] ] ≥ h e i g h t [ r k [ i − 1 ] ] − 1 height[rk[i]]\ge height[rk[i-1]]-1 height[rk[i]]height[rk[i1]]1
证明略。。。
所以我们按照排名来计算,即可做到 O ( n ) O(n) O(n)得到 h e i g h t [ ] height[] height[]

int k=0;
for (int i=1;i<=n;i++)
{
	if (k) k--;
	if (rk[i]==1) continue;
	int j=sa[rk[i]-1];
	while (s[j+k]==s[i+k]) k++;
	height[rk[i]]=k;
}

后缀数组的应用

最长公共前缀(LCP)

如果我们要求两个后缀的最长公共前缀,有如下方法。

性质: s u f f i x ( i ) suffix(i) suffix(i) s u f f i x ( j ) suffix(j) suffix(j)的最长公共前缀为 min ⁡ { h e i g h t [ r k [ i ] + 1 ] , h e i g h t [ r k [ i ] + 2 ] , ⋯ &ThinSpace; , h e i g h t [ r k [ j ] ] } \min\{height[rk[i]+1],height[rk[i]+2],\cdots,height[rk[j]]\} min{height[rk[i]+1],height[rk[i]+2],,height[rk[j]]}

所以我们根据求出的 h e i g h t height height数组,构造出ST表或线段树即可高效的查询。

小应用1

给定一个字符串,求至少在这个串中出现超过两次的最长子串长度(可重叠)。

思路:考虑这个出现超过两次的串,他一定是至少两个后缀的前缀,所以我们只需要求所有后缀的LCP的最大值即可。而我们的后缀数组是已经按照字典序排好的,相隔越近的,他的LCP才有可能更长,故所有最长的LCP,就是我们的 h e i g h t [ ] height[] height[]数组。所以只需要对 h e i g h t [ ] height[] height[]取max即可。

小应用2

上题的不可重叠版本。

思路:考虑二分这个串的长度 m i d mid mid
然后我们将 h e i g h t [ ] height[] height[]数组分组,分界线为 h e i g h t [ ] &lt; m i d height[]&lt;mid height[]<mid,单独考虑每一组,他们之间任意两个串都满足 L C P &gt; m i d LCP&gt;mid LCP>mid,那么我们只需要找到这一组中 s a [ ] sa[] sa[]的最大值和最小值,看是否相差超过 m i d mid mid即可。

例题

POJ 2217 Secretary

题目链接

题目大意

给定两个字符串,求这两个串的最长公共子串。

思路

子串肯定是一个后缀的前缀。
所以将两个字符串拼起来,中间加一个没有出现的字符'\0',然后求出后缀数组,求出 h e i g h t [ ] height[] height[]的最大值即可。

唔。。。好像有点毛病?如果这个 s a sa sa中相邻的两个串属于同一个原串就不合法了欸。
考虑记录分界线在哪里,对于所有 ( s a [ i ] &gt; p o s ) ≠ ( s a [ i + 1 ] &gt; p o s ) (sa[i]&gt;pos)\ne(sa[i+1]&gt;pos) (sa[i]>pos)̸=(sa[i+1]>pos)即两个端点分居两侧的后缀才统计答案即可。

代码
#include<cstdio>
#include<cstring>
#include<string>
#include<algorithm>
#include<iostream>
#include<cmath>
#include<cstdlib>
#include<ctime>
#include<map>
#include<queue>
#include<vector>
#include<stack>
#include<set>
#include<cctype>
#define pa pair<int,int>
#define INF 0x3f3f3f3f
#define inf 0x3f
#define fi first
#define se second
#define mp make_pair
#define ll long long
#define ull unsigned long long
#define pb push_back

using namespace std;

inline ll read()
{
	long long f=1,sum=0;
	char c=getchar();
	while (!isdigit(c)) {if (c=='-') f=-1;c=getchar();}
	while (isdigit(c)) {sum=sum*10+c-'0';c=getchar();}
	return sum*f;
}
const int MAXN=20010;
int wb[MAXN],x[MAXN],y[MAXN];
int rk[MAXN],sa[MAXN],height[MAXN];
char s[MAXN];
void build_sa()
{
	int n=strlen(s+1),m=10000,p=0;
	memset(wb,0,sizeof(wb));
	for (int i=1;i<=n;i++)
		wb[x[i]=s[i]]++;
	for (int i=1;i<=m;i++)
		wb[i]+=wb[i-1];
	for (int i=n;i;i--)
		sa[wb[x[i]]--]=i;
	for (int j=1;p<n;j*=2,m=p)
	{
		p=0;
		for (int i=n-j+1;i<=n;i++) y[++p]=i;
		for (int i=1;i<=n;i++) if (sa[i]>j) y[++p]=sa[i]-j;
		for (int i=1;i<=m;i++) wb[i]=0;
		for (int i=1;i<=n;i++) wb[x[y[i]]]++;
		for (int i=1;i<=m;i++) wb[i]+=wb[i-1];
		for (int i=n;i;i--) sa[wb[x[y[i]]]--]=y[i];
		for (int i=1;i<=n;i++) swap(x[i],y[i]);
		p=1,x[sa[1]]=1;
		for (int i=2;i<=n;i++)
		{
			if (y[sa[i]]==y[sa[i-1]] && y[sa[i]+j]==y[sa[i-1]+j]) x[sa[i]]=p;
			else x[sa[i]]=++p;
		}
	}
	for (int i=1;i<=n;i++)
		rk[sa[i]]=i;
	int k=0;
	for (int i=1;i<=n;i++)
	{
		if (k) k--;
		if (rk[i]==1) continue;
		int j=sa[rk[i]-1];
		while (s[j+k]==s[i+k]) k++;
		height[rk[i]]=k;
	}
} 
int main()
{
	int T;
	scanf("%d",&T);
	getchar();
	while (T--)
	{
		memset(s,0,sizeof(s)); 
		string s1,s2;
		s1.clear();s2.clear();
		getline(cin,s1);
		getline(cin,s2);
		int pos=s1.size();
		int n=0;	
		for (int i=0;i<(int)s1.size();i++)
			s[++n]=s1[i];
		s[++n]='~';
		for (int i=0;i<(int)s2.size();i++)
			s[++n]=s2[i];
		build_sa();
		int ans=0;
		for (int i=2;i<=n;i++)
			if ((sa[i]<pos)!=(sa[i-1]<pos))
				ans=max(ans,height[i]);
		printf("Nejdelsi spolecny retezec ma delku %d.\n",ans);
	}
	return 0;
}

Codeforces 432D

题目链接

题目大意

求一个字符串中所有既是前缀又是后缀的串在字符串中出现的次数。

思路

首先求出原串的后缀数组和高度数组。
然后枚举长度,哈希判断前后是否相等。
在高度数组上二分,查询这一段区间的 h e i g h t [ ] height[] height[]的最小值是否比枚举的长度大。
显然可以发现所有前缀是同一个字符串的后缀必定是连续的。
考虑只有 r k [ 1 ] rk[1] rk[1]在这段区间里的才能进行统计(否则和前缀不相等了)

代码
#include<cstdio>
#include<cstring>
#include<string>
#include<algorithm>
#include<iostream>
#include<cmath>
#include<cstdlib>
#include<ctime>
#include<map>
#include<queue>
#include<vector>
#include<stack>
#include<set>
#include<cctype>
#define pa pair<int,int>
#define INF 0x3f3f3f3f
#define inf 0x3f
#define fi first
#define se second
#define mp make_pair
#define ll long long
#define ull unsigned long long
#define pb push_back

using namespace std;

inline ll read()
{
	long long f=1,sum=0;
	char c=getchar();
	while (!isdigit(c)) {if (c=='-') f=-1;c=getchar();}
	while (isdigit(c)) {sum=sum*10+c-'0';c=getchar();}
	return sum*f;
}
const int MAXN=100010;
int wb[MAXN],x[MAXN],y[MAXN];
int sa[MAXN],height[MAXN],rk[MAXN];
char s[MAXN];
void build_sa()
{
	int n=strlen(s+1),m=10000,p=0;
	for (int i=1;i<=n;i++) wb[x[i]=s[i]]++;
	for (int i=1;i<=m;i++) wb[i]+=wb[i-1];
	for (int i=n;i>=1;i--) sa[wb[x[i]]--]=i;
	for (int j=1;p<n;j*=2,m=p)
	{
		p=0;
		for (int i=n-j+1;i<=n;i++) y[++p]=i;
		for (int i=1;i<=n;i++) if (sa[i]>j) y[++p]=sa[i]-j;
		for (int i=1;i<=m;i++) wb[i]=0;
		for (int i=1;i<=n;i++) wb[x[y[i]]]++;
		for (int i=1;i<=m;i++) wb[i]+=wb[i-1];
		for (int i=n;i>=1;i--) sa[wb[x[y[i]]]--]=y[i];
		for (int i=1;i<=n;i++) swap(x[i],y[i]);
		p=1,x[sa[1]]=1;
		for (int i=2;i<=n;i++)
		{
			if (y[sa[i]]==y[sa[i-1]] && y[sa[i]+j]==y[sa[i-1]+j]) x[sa[i]]=p;
			else x[sa[i]]=++p;
		}
	} 
	for (int i=1;i<=n;i++)
		rk[sa[i]]=i;
	int k=0;
	for (int i=1;i<=n;i++)
	{
		if (k) k--;
		if (rk[i]==1) continue;
		int j=sa[rk[i]-1];
		while (s[j+k]==s[i+k]) k++;
		height[rk[i]]=k;
	}
}
vector <pa> ans;
int f[4*MAXN]; 
void build(int root,int left,int right)
{
	if (left==right)
	{
		f[root]=height[left];
		return ;
	}
	int mid=(left+right)>>1;
	build(2*root,left,mid);
	build(2*root+1,mid+1,right);
	f[root]=min(f[2*root],f[2*root+1]); 
}
int query(int root,int left,int right,int qleft,int qright)
{
	if (qleft<=left && right<=qright)
		return f[root];
	int mid=(left+right)>>1;
	if (qright<=mid)
		return query(2*root,left,mid,qleft,qright);
	else if (qleft>mid)
		return query(2*root+1,mid+1,right,qleft,qright);
	else
	{
		int ans1,ans2;
		ans1=query(2*root,left,mid,qleft,mid);
		ans2=query(2*root+1,mid+1,right,mid+1,qright);
		return min(ans1,ans2);
	} 
}
int n;
bool check(int pos,int mid,int len)
{
	int st=pos,ed=pos+mid-1;
	if (ed<=st) return true;
	if (ed>n) return false;
	int qu=query(1,2,n,st+1,ed);
	return qu>=len;
}
ull hsh[MAXN],p[MAXN];
#define P 2333
ull get_hsh(int st,int len)
{
	return hsh[st+len-1]-hsh[st-1]*p[len];
}
int main()
{
	scanf("%s",s+1);
	build_sa();
	n=strlen(s+1);
	for (int i=1;i<=n;i++)
		hsh[i]=hsh[i-1]*P+(s[i]-'a'+1);
	p[0]=1;
	for (int i=1;i<=n;i++)
		p[i]=p[i-1]*P;
	if (n==1)
	{
		cout<<1<<endl;
		cout<<1<<' '<<1;
		return 0;
	}
	build(1,2,n);
	int pos=rk[1];
	for (int i=n;i>1;i--)
	{
		if (rk[i]>pos) continue;
		int l=1,r=n,anss,mid,len=n-i+1;
		if (get_hsh(1,len)!=get_hsh(i,len)) continue;
		while (l<=r)
		{
			mid=(l+r)>>1;
			if (check(rk[i],mid,len)) anss=mid,l=mid+1;
			else r=mid-1;
		}
		if (rk[i]+anss>pos) ans.push_back(mp(len,anss));
	}
	ans.push_back(mp(n,1));
	printf("%d\n",(int)ans.size());
	for (int i=0;i<ans.size();i++)
		printf("%d %d\n",ans[i].fi,ans[i].se);
	return 0;
}
/*
AAAAAAAAAAAAAAAAXAAAAAAAAAAAAAAAAAAAAAAA
*/
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值