ACM常用模板——数据结构——后缀数组

参考罗穗骞的论文

模板:

   
   
#include<cstdio>
#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<set>
#include<map>
#include<queue>
#include<vector>
#define maxn 1000005
#define pb push_back
#define LL longlong
#define MS(a,b) memset(a,b,sizeof(a))
#define FI(a,b) fill(a,a+maxn,b)
#define sf(n) scanf("%d",&n)
#define sf2(a,b) scanf("%d%d",&a,&b)
#define pf(n) printf("%d\n",n)
#define ffr(i,n)for(i=0;i<n;i++)
usingnamespace std;
intRank[maxn],tmp[maxn],sa[maxn],height[maxn];
int n,k;
bool compare_sa(int i,int j)
{
if(Rank[i]!=Rank[j])returnRank[i]<Rank[j];
else{
int ri=i+k<=n?Rank[i+k]:-1;
int rj=j+k<=n?Rank[j+k]:-1;
return ri<rj;
}
}
//计算字符串后缀数组sa
void constru_sa(string S)
{
int i;
n=S.length();
for(i=0;i<=n;i++){
sa[i]=i;
Rank[i]=i<n?S[i]:-1;
}
for(k=1;k<=n;k*=2){
sort(sa,sa+n+1,compare_sa);
tmp[sa[0]]=0;
for(i=1;i<=n;i++){
tmp[sa[i]]=tmp[sa[i-1]]+(compare_sa(sa[i-1],sa[i])?1:0);
}
for(i=0;i<=n;i++){
Rank[i]=tmp[i];
}
}
}
//高度数组height[i]:代表第i和i+1个后缀的最长公共前缀
void construct_lcp(string S)
{
int n=S.length(),i;
for(i=0;i<=n;i++)Rank[sa[i]]=i;
int h=0;
height[0]=0;
for(i=0;i<n;i++){
int j=sa[Rank[i]-1];
if(h>0)h--;
for(;j+h<n&&i+h<n;h++){
if(S[j+h]!=S[i+h])break;
}
height[Rank[i]]=h;
}
}
//字符串匹配
int contain(string S,string T)
{
int a=0,b=S.length();
while(b-a>1){
int c=(a+b)/2;
if(S.compare(sa[c],T.length(),T)<0)a=c;
else b=c;
}
if(S.compare(sa[b],T.length(),T)==0)return sa[b];
elsereturn-1;
}
int main()
{
string s,t;
int i;
while(cin>>s>>t){//s是原串,t是匹配串
constru_sa(s);
construct_lcp(s);
int len=s.size();
printf("sa: \n");
for(i=0;i<=len;i++){printf("%d ",sa[i]);}printf("\n");
printf("rank: \n");
for(i=0;i<=len;i++){printf("%d ",Rank[i]);}printf("\n");
printf("height: \n");
for(i=0;i<=len;i++){printf("%d ",height[i]);}printf("\n");
cout<<contain(s,t)<<endl;
}
return0;
}

(一)最长公共前缀

height[i]=suffix(sa[i-1])和suffix(sa[i])的最长公

共前缀,也就是排名相邻的两个后缀的最长公共前缀。那么对于j和k,不妨设

rank[j]<rank[k],则有以下性质:

suffix(j) 和 suffix(k) 的 最 长 公 共 前 缀 为 height[rank[j]+1],

height[rank[j]+2], height[rank[j]+3], … ,height[rank[k]]中的最小值。

例如,字符串为“aabaaaab”,求后缀“abaaaab”和后缀“aaab”的最长公共前缀

例如,字符串为“aabaaaab”,求后缀“abaaaab”和后缀“aaab”的最长公共前缀

例1:最长公共前缀

给定一个字符串,询问某两个后缀的最长公共前缀。

算法分析:

按照上面所说的做法,求两个后缀的最长公共前缀可以转化为求某个区间上的最小值。对于这个RMQ问题(如果对RMQ问题不熟悉,请阅读其他相关资料),可以用O(nlogn)的时间先预处理,以后每次回答询问的时间为O(1)。所以对于本问题,预处理时间为O(nlogn),每次回答询问的时间为O(1)。如果RMQ问题用O(n)的时间预处理,那么本问题预处理的时间可以做到O(n)。

hdu4691
后缀数组:寻找前一个区间与当前区间的前缀字符数目最多为多少
   
   
#include<cstdio>
#include<cstring>
#include<algorithm>
usingnamespace std;
#define LL longlong
#define N 100010
char s[N];
int m,n,len;
int t[N],t2[N],c[N],height[N],sa[N],rank[N],dp[N][20],cnt[N];
//sa 代表等级为i的下标
//rank 代表下标为i的等级
//height 代表字符串由最低位排序到最高位相邻两个字符串的前缀相同个数
 
void build_sa()
{
m=27;
int*x=t,*y=t2;
for(int i=0; i<m;++i) c[i]=0;
for(int i=0; i<n;++i)++c[x[i]=cnt[i]];
for(int i=1; i<m;++i) c[i]+=c[i-1];
for(int i=n-1; i>=0;--i) sa[--c[x[i]]]=i;
for(int k=1; k<=n; k<<=1)
{
int p=0;
for(int i=n-k; i<n;++i) y[p++]=i;
for(int i=0; i<n;++i)if(k<=sa[i]) y[p++]=sa[i]-k;
 
for(int i=0; i<m;++i) c[i]=0;
for(int i=0; i<n;++i)++c[x[y[i]]];
for(int i=1; i<m;++i) c[i]+=c[i-1];
for(int i=n-1; i>=0;--i) sa[--c[x[y[i]]]]=y[i];
 
swap(x,y);
p=1,x[sa[0]]=0;
for(int i=1; i<n;++i)
x[sa[i]]=y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k]?p-1:p++;
if(p>=n)break;
m=p;
}
}
 
void get_height()
{
for(int i=0; i<=len;++i) rank[sa[i]]=i;
for(int i=0,k=0; i<len;++i)
{
if(k)--k;
if(rank[i]-1<0)continue;
int j=sa[rank[i]-1];
while(cnt[i+k]==cnt[j+k])++k;
height[rank[i]]=k;
}
}
int RMQ(int L,int R)
{
int k=0;
while((1<<(k+1))<=R-L+1)++k;
return min(dp[L][k],dp[R-(1<<k)+1][k]);
}
 
void RMQ_init()
{
for(int i=0; i<=len;++i) dp[i][0]=height[i];
for(int j=1;(1<<j)<=len;++j)
for(int i=1; i+(1<<j)-1<=len;++i)
dp[i][j]=min(dp[i][j-1],dp[i+(1<<(j-1))][j-1]);
}
 
int lcp(int a,int l)
{
if(a==l)return len-a;
int u=rank[a],v=rank[l];
if(u>v)return RMQ(v+1,u);
elsereturn RMQ(u+1,v);
}
 
int cal(int p)
{
int v=p;
for(int i=1;;++i)
if(v/10==0)return i;
else v/=10;
}
int main()
{
// freopen("in.txt","r",stdin);
while(scanf("%s",s)!=EOF)
{
len=strlen(s);
for(int i=0; i<len;++i) cnt[i]=s[i]-'a'+1;
cnt[len]=0;
n=len+1;
build_sa();
get_height();
RMQ_init();
int t,a,b,l,r;
LL ans1,ans2;
scanf("%d",&t);
scanf("%d %d",&a,&b);
ans1=b-a+1;
ans2=b-a+3;
--t;
while(t--)
{
scanf("%d %d",&l,&r);
ans1=ans1+r-l+1;
int p=min(lcp(a,l),min(b-a,r-l));
ans2=ans2+(r-l)-p+2+cal(p);
a=l,b=r;
}
printf("%I64d %I64d\n",ans1,ans2);
}
return0;
}


(二)重复子串:
字符串R在字符串L中至少出现两次,则称R是L的重复子串。
例2:可重叠最长重复子串
给定一个字符串,求最长重复子串,这两个子串可以重叠。
算法分析:
这道题是后缀数组的一个简单应用。做法比较简单,只需要求height数组里的最大值即可。首先求最长重复子串,等价于求两个后缀的最长公共前缀的最大值。因为任意两个后缀的最长公共前缀都是 height数组里某一段的最小值,那么这个值一定不大于height数组里的最大值。所以最长重复子串的长度就是 height数组里的最大值。这个做法的时间复杂度为O(n)。


(三)不可重叠最长重复子串(pku1743)
给定一个字符串,求最长重复子串,这两个子串不能重叠。
算法分析:
这题比上一题稍复杂一点。先二分答案,把题目变成判定性问题:判断是否

存在两个长度为k的子串是相同的,且不重叠。解决这个问题的关键还是利用 height数组。把排序后的后缀分成若干组,其中每组的后缀之间的height值都不小于 k。例如,字符串为“aabaaaab”,当k=2时,后缀分成了 4组,如图 5 所示。

容易看出,有希望成为最长公共前缀不小于k的两个后缀一定在同一组。然后对于每组后缀,只须判断每个后缀的sa值的最大值和最小值之差是否不小于 k。如果有一组满足,则说明存在,否则不存在。整个做法的时间复杂度为 O(nlogn)。本题中利用height值对后缀进行分组的方法很常用,请读者认真体会。

    
    
#define maxn 20010
int wa[maxn],wb[maxn],wv[maxn],wss[maxn];
int r[maxn],sa[maxn];
int cmp(int*r,int a,int b,int l)
{return r[a]==r[b]&& r[a+l]==r[b+l];}
/*【倍增算法O(nlgn)】待排序的字符串放在r 数组中,从r[0]到r[n-1],长度为n,且最大值小于m
使用倍增算法前,需要保证r数组的值均大于0。然后要在原字符串后添加一个0号字符
所以,若原串的长度为n,则实际要进行后缀数组构建的r数组的长度应该为n+1.所以调用da函数时,对应的n应为n+1.
*/
void da(int*r,int*sa,int n,int m){//n要加1
int i,j,p,*x=wa,*y=wb,*t;
for(i=0;i<m;i++) wss[i]=0;
for(i=0;i<n;i++) wss[x[i]=r[i]]++;
for(i=1;i<m;i++) wss[i]+=wss[i-1];
for(i=n-1;i>=0;i--) sa[--wss[x[i]]]=i;
for(j=1,p=1;p<n;j*=2,m=p){
for(p=0,i=n-j;i<n;i++) y[p++]=i;
for(i=0;i<n;i++)if(sa[i]>=j) y[p++]=sa[i]-j;
for(i=0;i<n;i++) wv[i]=x[y[i]];
for(i=0;i<m;i++) wss[i]=0;
for(i=0;i<n;i++) wss[wv[i]]++;
for(i=1;i<m;i++) wss[i]+=wss[i-1];
for(i=n-1;i>=0;i--) sa[--wss[wv[i]]]=y[i];
for(t=x,x=y,y=t,p=1,x[sa[0]]=0,i=1;i<n;i++)
x[sa[i]]=cmp(y,sa[i-1],sa[i],j)?p-1:p++;
}
return;
}
int rank[maxn],height[maxn];//rank[i]:i排第几;sa[i]:排第i的后缀串在哪里,互为逆运算
 
void calheight(int*r,int*sa,int n){//n不用加1
int i,j,k=0;
for(i=1;i<=n;i++) rank[sa[i]]=i;
for(i=0;i<n;height[rank[i++]]=k){
for(k?k--:0,j=sa[rank[i]-1];r[i+k]==r[j+k];k++);
}
return;
}
 
int main(){
int n;
while(scanf("%d",&n)&& n){
int i;
int x,y;
scanf("%d",&x);
for(i=0;i<n-1;i++){
scanf("%d",&y);
r[i]= y - x +100;//不能出现负数
x = y;
}cout<<endl;
r[n-1]=0;//总共有n-1个值
da(r,sa,n,190);
calheight(r,sa,n-1);
for(i=1;i<=n-1;i++)
cout<<height[i]<<endl;
int l =0,r = n-1;
int mid;
int ans=0;
while(l<=r){
mid =(l+r)>>1;
int minm = sa[1];
int maxm = sa[1];
bool ok =0;
for(i=2;i<=n;i++){//不写i<=n-1的原因系这样可以处理完最后一组
if(height[i]>=mid && i!=n){
if(sa[i]>maxm)maxm = sa[i];
if(sa[i]<minm)minm = sa[i];
}else{
if(maxm - minm>=mid){
ok =1;
break;
}else minm = maxm = sa[i];
}
}
if(ok){
ans = mid;
l = mid+1;
}else r = mid-1;
}
//ans+1的原因是前面的处理是把后一个减去前一个的值,所以最后要+1个
if(ans+1<=4)printf("%d\n",0);
else printf("%d\n",ans+1);
}
return0;
}
(四)可重叠的k次最长重复子串(pku3261)

给定一个字符串,求至少出现k次的最长重复子串,这k个子串可以重叠。

算法分析:

这题的做法和上一题差不多,也是先二分答案,然后将后缀分成若干组。不同的是,这里要判断的是有没有一个组的后缀个数不小于k。如果有,那么存在 k个相同的子串满足条件,否则不存在。这个做法的时间复杂度为O(nlogn)。

    
    
/*
PKU3261 Milk Patterns
*/
#include<stdio.h>
#include<memory.h>
#include<stdlib.h>
#define MAX(a,b)((a)>(b)?(a):(b))
#define MIN(a,b)((a)>(b)?(b):(a))
#define N 20005
#define K 35
int nday;
/***************************************/
int power;
int lstrank[N];
int cmpstr[N];
int cmpChar(constvoid*a,constvoid*b){
return cmpstr[*(int*)a]- cmpstr[*(int*)b];
}
int cmpLst(constvoid*A,constvoid*B){
int a =*(int*)A;
int b =*(int*)B;
if(lstrank[a]==lstrank[b])
return lstrank[a+power]-lstrank[b+power];
return lstrank[a]-lstrank[b];
}
/*后缀数组
参数:
输入字符串str[];
返回后缀数组sa[];返回名次数组rank[]
说明:
复杂度O(nlogn)
需要stdlib.h辅助全局变量power, lstrank[],cmpstr[]
*/
void suffixArray(int str[],int sa[],int rank[]){
int i,j,n=nday;
for(i=0;i<n;i++) sa[i]=i;
memcpy(cmpstr,str,sizeof(cmpstr));
qsort(sa,n,sizeof(int),cmpChar);
for(i=j=0;i<n;i++){
if(i>0&& str[sa[i]]!=str[sa[i-1]]) j++;
rank[sa[i]]=j;
}
for(power=1;rank[sa[n-1]]<n-1; power*=2)
{
memcpy(lstrank,rank,sizeof(int)*n);
qsort(sa,n,sizeof(int), cmpLst);
for(i=j=0;i<n;i++){
if(i>0&&cmpLst(&sa[i-1],&sa[i])) j++;
rank[sa[i]]=j;
}
}
}
/*区间最大值询问
preRMQ()用O(nlogn)时间预处理
RMQ()用O(1)的时间询问i~j之间的最大值
*/
int dr[N][K]={0};
void preRMQ(int a[],int n){
int i,j,k,f,s;
for(i=0;i<n;i++) dr[i][0]=a[i];
for(s=f=1;s<n;f++){
for(i=0;;i++){
if(i+s>=n)break;
dr[i][f]= MIN(dr[i][f-1],dr[i+s][f-1]);
}
s=1<<f;
}
}
int RMQ(int p,int q){
int d,f;
d=q-p+1;
for(f=0;(1<<f)<=d;f++); f--;
return MIN(dr[p][f],dr[q-(1<<f)+1][f]);
}
/*预处理LCP求h[],height[]
参数:
输入原字符串str[]
输入后缀数组sa[]
输入名次数组rank[]
返回height[]
说明:
复杂度O(n)
需要事先用suffixArray计算sa[]和rank[]
height[i]=LCP(i-1,i)
h[i]=height[rank[i]]
height[i]=h[sa[i]]
*/
void preLCP(int str[],int sa[],int rank[],int height[]){
int i,j,k,s,n=nday;
int h[N];
 
for(i=0;i<n;i++){
if(rank[i]==0){
h[i]=0;continue;
}
j=rank[i]-1;
k=rank[i];
if(i==0||h[i-1]<=1) s=0;
else s=h[i-1]-1;
for(;sa[k]+s<n && sa[j]+s<n;s++)
if(str[sa[k]+s]!= str[sa[j]+s])break;
h[i]=s;
}
for(i=0;i<n;i++) height[rank[i]]= h[i];
preRMQ(height,n);
}
/*
LCP询问后缀数组sa[]中x和y的最长公共前缀,复杂度O(logn)
*/
int LCP(int x,int y){
if(x<y)return RMQ(x+1,y);
elsereturn RMQ(y+1,x);
}
/**************************************/
int n,m;
int str[N];
int sa[N];
int rank[N];
int height[N];
void printa(int a[]){
int i;
for(i=0;a[i];i++) printf("%d",a[i]-1);
puts("");
}
int main()
{
int i,j,k;
int ans;
 
while(scanf("%d%d",&n,&m)!=EOF){
nday=n+1;
for(i=0;i<n;i++){
scanf("%d",&str[i]); str[i]++;
}
str[n]=0;
suffixArray(str,sa,rank);
preLCP(str,sa,rank,height);
/*
for(i=0;i<n;i++){
printf("%d - %d: ",i,sa[i]);
printa(str+sa[i]);
}*/
ans=0;
for(i=0,j=m-1;j<=n;i++,j++){
k=LCP(i,j);
ans=MAX(ans,k);
}
printf("%d/n",ans);
}
return0;
}
(五)

不相同的子串的个数(spoj694,spoj705)

给定一个字符串,求不相同的子串的个数。

算法分析:

每个子串一定是某个后缀的前缀,那么原问题等价于求所有后缀之间的不相同的前缀的个数。如果所有的后缀按照suffix(sa[1]), suffix(sa[2]), suffix(sa[3]), …… ,suffix(sa[n])的顺序计算,不难发现,对于每一次新加进来的后缀suffix(sa[k]),它将产生 n-sa[k]+1 个新的前缀。但是其中有 height[k]个是和前面的字符串的前缀是相同的。所以suffix(sa[k])将“贡献” 出n-sa[k]+1- height[k]个不同的子串。累加后便是原问题的答案。这个做法的时间复杂度为O(n)。

     
     
/*
*Author: Zhaofa Fang
*Created time: 2013-04-21-21.19
*Language: C++
*/
#include<cstdio>
#include<cstdlib>
#include<sstream>
#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<string>
#include<utility>
#include<vector>
#include<queue>
#include<map>
#include<set>
usingnamespace std;
 
typedeflonglong ll;
#define DEBUG(x) cout<<#x << ':' << x << endl
#define FOR(i,s,t)for(int i =(s);i <=(t);i++)
#define FORD(i,s,t)for(int i =(s);i >=(t);i--)
#define REP(i,n) FOR(i,0,n-1)
#define REPD(i,n) FORD(i,n-1,0)
#define PII pair<int,int>
#define PB push_back
#define MP make_pair
#define ft first
#define sd second
#define lowbit(x)(x&(-x))
#define INF (1<<30)
 
constint maxn =1111;
char s[maxn];
int sa[maxn],t1[maxn],t2[maxn],c[maxn];
int rank[maxn],height[maxn];
 
void getHeight(int n){
int k =0;
for(int i=1;i<=n;i++)rank[sa[i]]= i;
for(int i=0;i<n;i++){
if(k)k--;
int j = sa[rank[i]-1];
while(s[i+k]==s[j+k])k++;
height[rank[i]]= k;
}
}
bool cmp(int*r,int a,int b,int l){
return(r[a]==r[b]&& r[a+l]==r[b+l]);
}
void build_sa(int m,int n){
int i,*x=t1,*y=t2,k,p;
for( i=0;i<m;i++)c[i]=0;
for( i=0;i<n;i++)c[x[i]= s[i]]++;
for( i=1;i<m;i++)c[i]+= c[i-1];
for( i=n-1;i>=0;i--)sa[-- c[x[i]]]= i;
for(k=1,p=0;p<n;m=p,k<<=1){
p =0;
for(i=n-k;i<n;i++)y[p++]= i;
for(i=0;i<n;i++)if(sa[i]>=k)y[p++]= sa[i]-k;
for(i=0;i<m;i++)c[i]=0;
for(i=0;i<n;i++)c[x[y[i]]]++;
for(i=1;i<m;i++)c[i]+= c[i-1];
for(i=n-1;i>=0;i--)sa[--c[x[y[i]]]]= y[i];
swap(x,y);
p =1; x[sa[0]]=0;
for(i=1;i<n;i++)
x[sa[i]]= cmp(y,sa[i-1],sa[i],k)?p-1:p++;
}
getHeight(n-1);
}
int solve(int n){
int ans = n - sa[1];
for(int i=2;i<=n;i++){
ans += n-sa[i]-height[i];
}
return ans;
}
int main(){
//freopen("in","r",stdin);
//freopen("out","w",stdout);
int T;
cin>>T;
while(T--){
scanf("%s",s);
int n = strlen(s);
build_sa(255,n+1);
printf("%d\n",solve(n));
}
return0;
}
(六)
回文子串:如果将字符串L的某个子字符串R反过来写后和原来的字符串R

一样,则称字符串R是字符串L的回文子串。

例6:最长回文子串(ural1297)

给定一个字符串,求最长回文子串。

算法分析:

穷举每一位,然后计算以这个字符为中心的最长回文子串。注意这里要分两种情况,一是回文子串的长度为奇数,二是长度为偶数。两种情况都可以转化为求一个后缀和一个反过来写的后缀的最长公共前缀。具体的做法是:将整个字符串反过来写在原字符串后面,中间用一个特殊的字符隔开。这样就把问题变为了求这个新的字符串的某两个后缀的最长公共前缀。如图6所示。

这个做法的时间复杂度为O(nlogn)。如果RMQ问题用时间为O(n)的方法预

处理,那么本题的时间复杂度可以降为O(n)。

     
     
#include<stdio.h>
#include<iostream>
#include<string.h>
#include<algorithm>
#define MS(a,b) memset(a,b,sizeof(a))
usingnamespace std;
constint maxn=200010;
char s[maxn];
int w[maxn],wa[maxn],wb[maxn],wv[maxn],x[maxn],sa[maxn],Rank[maxn],height[maxn],n;
int cmp(int*r,int a,int b,int l)
{
return r[a]==r[b]&&r[a+l]==r[b+l];
}
void da(int n,int m)//n:L,m:kind
{
int i,j,p,*x=wa,*y=wb,*t;
for(i=0; i<m; i++) w[i]=0;
for(i=0; i<n; i++) w[x[i]=s[i]]++;
for(i=1; i<m; i++) w[i]+=w[i-1];
for(i=n-1; i>=0; i--) sa[--w[x[i]]]=i;
for(p=1,j=1; p<n; j*=2,m=p)
{
for(p=0,i=n-j; i<n; i++) y[p++]=i;
for(i=0; i<n; i++)if(sa[i]>=j) y[p++]=sa[i]-j;
for(i=0; i<m; i++) w[i]=0;
for(i=0; i<n; i++) w[wv[i]=x[y[i]]]++;
for(i=1; i<m; i++) w[i]+=w[i-1];
for(i=n-1; i>=0; i--) sa[--w[wv[i]]]=y[i];
for(t=x,x=y,y=t,p=1,i=1,x[sa[0]]=0; i<n; i++)
x[sa[i]]=cmp(y,sa[i-1],sa[i],j)?p-1:p++;
}
return;
}
void cal(int n)
{
int i,j,k=0;
for(i=1; i<=n; i++)Rank[sa[i]]=i;
for(i=0; i<n; height[Rank[i++]]=k)
for(k?k--:0,j=sa[Rank[i]-1]; s[i+k]==s[j+k]; k++);
return;
}
int main()
{
//freopen("F:\\in.txt","r",stdin);
int len,l,ans;
while(~scanf("%s",s))
{
l=strlen(s);
char s1[maxn];
MS(s1,0);
strcpy(s1,s);
reverse(s1,s1+strlen(s1));
s[l]='#';
strncat(s,s1,l);
len=strlen(s);
da(len+1,'z'+1);//sa start 0
cal(len);
ans=0;
//cout<<s<<endl;
for(int i=1;i<=len;i++)
{
//cout<<sa[i]<<' '<<sa[i-1]<<' '<<ans<<endl;
if(sa[i]<l&&sa[i-1]>l||sa[i]>l&&sa[i-1]<l)
ans=max(ans,height[i]);
}
printf("%d\n",ans);
MS(s,0);
}
return0;
}
(七)连续重复串:如果一个字符串L是由某个字符串S重复R次而得到的,则称

L是一个连续重复串。R是这个字符串的重复次数。

例7:连续重复子串(pku2406)

给定一个字符串L,已知这个字符串是由某个字符串S重复R次而得到的,

求R的最大值。

算法分析:

做法比较简单,穷举字符串S的长度k,然后判断是否满足。判断的时候,先看字符串L的长度能否被k整除,再看suffix(1)和suffix(k+1)的最长公共前缀是否等于n-k。在询问最长公共前缀的时候,suffix(1)是固定的,所以RMQ 问题没有必要做所有的预处理,只需求出 height 数组中的每一个数到 height[rank[1]]之间的最小值即可。整个做法的时间复杂度为O(n)。




(八)重复次数最多的连续重复子串(spoj687,pku3693)

给定一个字符串,求重复次数最多的连续重复子串。

算法分析:

先穷举长度L,然后求长度为L的子串最多能连续出现几次。首先连续出现 1次是肯定可以的,所以这里只考虑至少2次的情况。假设在原字符串中连续出现 2 次,记这个子字符串为 S,那么 S 肯定包括了字符 r[0], r[L], r[L*2], r[L*3], ……中的某相邻的两个。所以只须看字符r[L*i]和r[L*(i+1)]往前和往后各能匹配到多远,记这个总长度为K,那么这里连续出现了K/L+1次。最后看最大值是多少。如图7所示。

穷举长度L的时间是n,每次计算的时间是n/L。所以整个做法的时间复杂度是O(n/1+n/2+n/3+……+n/n)=O(nlogn)。

     
     
// File Name: 3693.cpp
// Author: Zlbing
// Created Time: 2013年09月06日 星期五 21时05分32秒
 
#include<iostream>
#include<string>
#include<algorithm>
#include<cstdlib>
#include<cstdio>
#include<set>
#include<map>
#include<vector>
#include<cstring>
#include<stack>
#include<cmath>
#include<queue>
usingnamespace std;
#define CL(x,v); memset(x,v,sizeof(x));
#define INF 0x3f3f3f3f
#define LL longlong
#define REP(i,r,n)for(int i=r;i<=n;i++)
#define RREP(i,n,r)for(int i=n;i>=r;i--)
//rank从0开始
//sa从1开始,因为最后一个字符(最小的)排在第0位
//height从2开始,因为表示的是sa[i-1]和sa[i]
constint MAXN=220000;
int rank[MAXN],sa[MAXN],X[MAXN],Y[MAXN],height[MAXN];
char s[MAXN];
int buc[MAXN];
void calheight(int n){
int i , j , k =0;
for(i =1; i <= n ; i++) rank[sa[i]]= i;
for(i =0; i < n ; height[rank[i++]]= k)
for(k?k--:0, j = sa[rank[i]-1]; s[i+k]== s[j+k]; k++);
}
bool cmp(int*r,int a,int b,int l){
return(r[a]== r[b]&& r[a+l]== r[b+l]);
}
void suffix(int n,int m =128){
int i , l , p ,*x = X ,*y = Y;
for(i =0; i < m ; i ++) buc[i]=0;
for(i =0; i < n ; i ++) buc[ x[i]= s[i]]++;
for(i =1; i < m ; i ++) buc[i]+= buc[i-1];
for(i = n -1; i >=0; i --) sa[--buc[ x[i]]]= i;
for(l =1,p =1; p < n ; m = p , l *=2){
p =0;
for(i = n-l ; i < n ; i ++) y[p++]= i;
for(i =0; i < n ; i ++)if(sa[i]>= l) y[p++]= sa[i]- l;
for(i =0; i < m ; i ++) buc[i]=0;
for(i =0; i < n ; i ++) buc[ x[y[i]]]++;
for(i =1; i < m ; i ++) buc[i]+= buc[i-1];
for(i = n -1; i >=0; i --) sa[--buc[ x[y[i]]]]= y[i];
for(swap(x,y), x[sa[0]]=0, i =1, p =1; i < n ; i ++)
x[ sa[i]]= cmp(y,sa[i-1],sa[i],l)? p-1: p++;
}
calheight(n-1);//后缀数组关键是求出height,所以求sa的时候顺便把rank和height求出来
}
//当需要反复询问两个后缀的最长公共前缀时用到RMQ
intLog[MAXN];
int best[20][MAXN];
void initRMQ(int n){//初始化RMQ
for(int i =1; i <= n ; i ++) best[0][i]= height[i];
for(int i =1; i <=Log[n]; i ++){
int limit = n -(1<<i)+1;
for(int j =1; j <= limit ; j ++){
best[i][j]= min(best[i-1][j], best[i-1][j+(1<<i>>1)]);
}
}
}
int lcp(int a,int b){//询问a,b后缀的最长公共前缀
a = rank[a]; b = rank[b];
if(a > b) swap(a,b);
a ++;
int t =Log[b - a +1];
return min(best[t][a], best[t][b -(1<<t)+1]);
}
int main(){
//预处理每个数字的Log值,常数优化,用于RMQ
Log[0]=-1;
for(int i =1; i < MAXN ; i ++){
Log[i]=(i&(i-1))?Log[i-1]:Log[i-1]+1;
}
int cas=0;
//*******************************************
// n为数组长度,下标0开始
// 将初始数据,保存在s里,并且保证每个数字都比0大
// m = max{ s[i] } + 1
// 一般情况下大多是字符操作,所以128足够了
//*******************************************
while(~scanf("%s",s))
{
if(s[0]=='#')break;
int n=strlen(s);
s[n]=0;
suffix(n+1);
initRMQ(n);
int maxx=0;
int len=0;
int pos=0;
for(int i=1;i<=n/2;i++)
{
for(int j=0;j+i<n;j+=i)
{
if(s[j]!=s[j+i])continue;
int k=lcp(j,j+i);
int tot=k/i+1;
int t=i-(k%i);
int p=j;
int cnt=0;
if(t&&t<i)
{
for(int m=j-1;m>j-i&&s[m]==s[m+i]&&m>=0;m--)
{
cnt++;
if(cnt==t)
{
tot++;
p=m;
}
elseif(rank[p]>rank[m])
{
p=m;
}
}
}
if(tot>maxx)
{
maxx=tot;
pos=p;
len=maxx*i;
}
elseif(tot==maxx)
{
if(rank[p]<rank[pos])
{
pos=p;
len=maxx*i;
}
}
}
}
printf("Case %d: ",++cas);
if(maxx<2)
{
char ch='z';
for(int i=0;i<n;i++)
if(s[i]<ch)
ch=s[i];
printf("%c\n",ch);
continue;
}
for(int i=pos;i<pos+len;i++)
printf("%c",s[i]);
printf("\n");
}
 
return0;
}

(九)公共子串:如果字符串L同时出现在字符串A和字符串B中,则称字符串L 是字符串A和字符串B的公共子串。

例9:最长公共子串(pku2774,ural1517)

给定两个字符串A和B,求最长公共子串。

算法分析:

字符串的任何一个子串都是这个字符串的某个后缀的前缀。求A和B的最长公共子串等价于求 A的后缀和 B的后缀的最长公共前缀的最大值。如果枚举 A 和B的所有的后缀,那么这样做显然效率低下。由于要计算A的后缀和B的后缀的最长公共前缀,所以先将第二个字符串写在第一个字符串后面,中间用一个没有出现过的字符隔开,再求这个新的字符串的后缀数组。观察一下,看看能不能从这个新的字符串的后缀数组中找到一些规律。以A=“aaaba”,B=“abaa”为例,如图8所示。

那么是不是所有的height值中的最大值就是答案呢?不一定!有可能这两个后缀是在同一个字符串中的,所以实际上只有当 suffix(sa[i-1])和 suffix(sa[i])不是同一个字符串中的两个后缀时,height[i]才是满足条件的。而这其中的最大值就是答案。记字符串A和字符串B的长度分别为|A|和|B|。求新的字符串的后缀数组和height数组的时间是O(|A|+|B|),然后求排名相邻但原来不在同一个字符串中的两个后缀的 height 值的最大值,时间也是 O(|A|+|B|),所以整个做法的时间复杂度为 O(|A|+|B|)。时间复杂度已经取到下限,由此看出,这是一个非常优秀的算法。

子串的个数

例10:长度不小于k的公共子串的个数(pku3415)

给定两个字符串A和B,求长度不小于k的公共子串的个数(可以相同)。

样例1:

A=“xx”,B=“xx”,k=1,长度不小于k的公共子串的个数是5。

样例2:

A=“aababaa”,B=“abaabaa”,k=2,长度不小于k的公共子串的个数是22。

算法分析:

基本思路是计算A的所有后缀和B的所有后缀之间的最长公共前缀的长度,

把最长公共前缀长度不小于k的部分全部加起来。先将两个字符串连起来,中间用一个没有出现过的字符隔开。按height值分组后,接下来的工作便是快速的统计每组中后缀之间的最长公共前缀之和。扫描一遍,每遇到一个B的后缀就统计与前面的A的后缀能产生多少个长度不小于k的公共子串,这里A的后缀需要用一个单调的栈来高效的维护。然后对A也这样做一次。具体的细节留给读者思考。

     
     
#include<stdio.h>
#include<iostream>
#include<string.h>
#include<algorithm>
#define MS(a,b) memset(a,b,sizeof(a))
usingnamespace std;
constint maxn=200010;
char s[maxn];
int w[maxn],wa[maxn],wb[maxn],wv[maxn],x[maxn],sa[maxn],Rank[maxn],height[maxn],n;
int cmp(int*r,int a,int b,int l)
{
return r[a]==r[b]&&r[a+l]==r[b+l];
}
void da(int n,int m)//n:L,m:kind
{
int i,j,p,*x=wa,*y=wb,*t;
for(i=0; i<m; i++) w[i]=0;
for(i=0; i<n; i++) w[x[i]=s[i]]++;
for(i=1; i<m; i++) w[i]+=w[i-1];
for(i=n-1; i>=0; i--) sa[--w[x[i]]]=i;
for(p=1,j=1; p<n; j*=2,m=p)
{
for(p=0,i=n-j; i<n; i++) y[p++]=i;
for(i=0; i<n; i++)if(sa[i]>=j) y[p++]=sa[i]-j;
for(i=0; i<m; i++) w[i]=0;
for(i=0; i<n; i++) w[wv[i]=x[y[i]]]++;
for(i=1; i<m; i++) w[i]+=w[i-1];
for(i=n-1; i>=0; i--) sa[--w[wv[i]]]=y[i];
for(t=x,x=y,y=t,p=1,i=1,x[sa[0]]=0; i<n; i++)
x[sa[i]]=cmp(y,sa[i-1],sa[i],j)?p-1:p++;
}
return;
}
void cal(int n)
{
int i,j,k=0;
for(i=1; i<=n; i++)Rank[sa[i]]=i;
for(i=0; i<n; height[Rank[i++]]=k)
for(k?k--:0,j=sa[Rank[i]-1]; s[i+k]==s[j+k]; k++);
return;
}
int main()
{
//freopen("F:\\in.txt","r",stdin);
int len,l,ans,t,m,r,i,ll,rr;
scanf("%d",&t);
while(t--)
{
scanf("%s%d",s,&m);
len=strlen(s);
da(len+1,'z'+1);//sa start 0
cal(len);
while(m--){
scanf("%d%d",&l,&r);
int d=r-l+1;
ans=0;
ll=Rank[l-1];
rr=Rank[r-1];
if(ll>rr)swap(rr,ll);
int flag=0;
for(i=0;i<=len;i++){
ans+=d-sa[i]-height[i];
//cout<<i<<' '<<sa[i]<<' '<<l<<' '<<r<<' '<<height[i]<<endl;
}
printf("%d\n",ans);
}
}
return0;
}
 

2.4多个字符串的相关问题

这类问题的一个常用做法是,先将所有的字符串连接起来,然后求后缀数组和height数组,再利用height数组进行求解。这中间可能需要二分答案。

例11:不小于k个字符串中的最长子串(pku3294) 给定n个字符串,求出现在不小于k个字符串中的最长子串。

算法分析:

将n个字符串连起来,中间用不相同的且没有出现在字符串中的字符隔开,

求后缀数组。然后二分答案,用和例3同样的方法将后缀分成若干组,判断每组的后缀是否出现在不小于k个的原串中。这个做法的时间复杂度为O(nlogn)。

例12:每个字符串至少出现两次且不重叠的最长子串(spoj220)

给定n个字符串,求在每个字符串中至少出现两次且不重叠的最长子串。

算法分析:

做法和上题大同小异,也是先将n个字符串连起来,中间用不相同的且没有出现在字符串中的字符隔开,求后缀数组。然后二分答案,再将后缀分组。判断的时候,要看是否有一组后缀在每个原来的字符串中至少出现两次,并且在每个原来的字符串中,后缀的起始位置的最大值与最小值之差是否不小于当前答案

(判断能否做到不重叠,如果题目中没有不重叠的要求,那么不用做此判断)。

这个做法的时间复杂度为O(nlogn)。

例13:出现或反转后出现在每个字符串中的最长子串(PKU3294)

给定n个字符串,求出现或反转后出现在每个字符串中的最长子串。

算法分析:

这题不同的地方在于要判断是否在反转后的字符串中出现。其实这并没有加大题目的难度。只需要先将每个字符串都反过来写一遍,中间用一个互不相同的且没有出现在字符串中的字符隔开,再将n个字符串全部连起来,中间也是用一个互不相同的且没有出现在字符串中的字符隔开,求后缀数组。然后二分答案,再将后缀分组。判断的时候,要看是否有一组后缀在每个原来的字符串或反转后的字符串中出现。这个做法的时间复杂度为O(nlogn)。


子串出现两个以上的个数:
     
     
#include<stdio.h>
#include<iostream>
#include<cstring>
#include<map>
#define MS(a,b) memset(a,b,sizeof(a))
#define INF 1<<29
usingnamespace std;
constint maxn=200010;
string s;
int w[maxn],wa[maxn],wb[maxn],wv[maxn],x[maxn],sa[maxn],Rank[maxn],height[maxn],n;
int cmp(int*r,int a,int b,int l)
{
return r[a]==r[b]&&r[a+l]==r[b+l];
}
void da(int n,int m)//n:L,m:kind
{
int i,j,p,*x=wa,*y=wb,*t;
for(i=0; i<m; i++) w[i]=0;
for(i=0; i<n; i++) w[x[i]=s[i]]++;
for(i=1; i<m; i++) w[i]+=w[i-1];
for(i=n-1; i>=0; i--) sa[--w[x[i]]]=i;
for(p=1,j=1; p<n; j*=2,m=p)
{
for(p=0,i=n-j; i<n; i++) y[p++]=i;
for(i=0; i<n; i++)if(sa[i]>=j) y[p++]=sa[i]-j;
for(i=0; i<m; i++) w[i]=0;
for(i=0; i<n; i++) w[wv[i]=x[y[i]]]++;
for(i=1; i<m; i++) w[i]+=w[i-1];
for(i=n-1; i>=0; i--) sa[--w[wv[i]]]=y[i];
for(t=x,x=y,y=t,p=1,i=1,x[sa[0]]=0; i<n; i++)
x[sa[i]]=cmp(y,sa[i-1],sa[i],j)?p-1:p++;
}
return;
}
void cal(int n)
{
int i,j,k=0;
for(i=1; i<=n; i++)Rank[sa[i]]=i;
for(i=0; i<n; height[Rank[i++]]=k)
for(k?k--:0,j=sa[Rank[i]-1]; s[i+k]==s[j+k]; k++);
return;
}
int solve(int i,int n)
{
int ans=0,max1=-1,min1=INF,j;
for(j=2;j<=n;j++)
{
if(height[j]>=i)//由于height[i]是rank[i]和rank[i]-1的最长公共前缀,rank是排序了,所以
{//如果连续if而没有else,重复子串也不会变
max1=max(sa[j-1],max(max1,sa[j]));
min1=min(sa[j-1],min(min1,sa[j]));
}
else
{
if(min1!=INF&&max1!=-1&&max1-min1>=i)//判断长度为i的重复子串是否重叠
ans++;
max1=-1;
min1=INF;
}
}
if(min1!=INF&&max1!=-1&&max1-min1>=i)
ans++;
return ans;
}
int main()
{
//freopen("F:\\in.txt","r",stdin);
int l,j,k;
while(cin>>s)
{
if(s=="#")break;
l=s.size();
s[l]='#';
da(l+1,'z'+1);//sa start 0
cal(l);
__int64 ans=0;
for(int i=1;i<=l/2;i++){
ans+=solve(i,l);
}
printf("%I64d\n",ans);
}
return0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值