题目
重复次数R定义为一个串中重复的最大次数,
例如,abababab可以看做是R=4的ab的重复,此时输出abababab
而abcdabcd可以看成是R=2的abcd的重复,
给定一个长为n(n<=1e5)的小写字母串,
输出其子串中满足R是最大的,
且该子串是所有R最大的串中字典序最小的这个子串
思路来源
罗穗骞《后缀数组——处理字符串的有力工具》
题解

图片引自罗穗骞论文《后缀数组——处理字符串的有力工具》,感觉很清楚了,但没讲往前匹配怎么弄
首先,构建后缀数组,构建sa、height、rank,预处理倍增rmq、lcp等等,
这个板子所有下标都是[0,n-1]的,且不用后面补一个空字符
枚举长度i,起点j,出现次数至少为2的,lcp(j-i,j)一定会大于等于i
j-i和j的lcp显然是往后接可行的最大长度,[j-i,j)[j,j+lcp),长为i+lcp,出现次数num为(i+lcp)/i
但lcp%i不为0即不能整除时,意味着把j-i往左挪有可能仍然是这个num,但可能字典序更小,
所以,此时保留这个长度,最后再去按sa增序遍历,
特别的,如果向左挪i-lcp%i,
即补全不能整除的剩下的长度部分,也能满足lcp的对应要求时,说明此时还可以多凑齐一个循环节,num++
更新最大次数,最大次数相同时,把所有可行长度都加到数组里,
然后按字典序sa增序枚举下标i,如果对于i来说存在一个长度lenj满足要求就输出答案
显然,对于i相同来说,越短的前缀字典序越小,重复次数相同即越短的越小,
而两个合法的串来说,sa[i]开头的长度为len*mx的串,是不会比sa[i+1]开头的长度为(len-1)*mx字典序更大的
反证法可以证明,这违反了sa[i]<sa[i+1],
然后就有先按字典序增序找,再按长度增序找,找到第一个输出即可的正确性
虽然代码统一了最大出现次数为1的情况,但可以采取一点小技巧使时间更短
先置最大出现次数为1,长度为1,这样可以省去对其他的出现次数为1的判断
复杂度有点迷,但很难卡掉就是了
还有一种做法是,后面的部分仍然用LCP,前面的部分由于有一段是出现次数相同的,
此时就从j-i开始往回跳,一个一个往前比较,如果j-i-1出现次数相同且rank更小就改变左端点为j-i-1
此题有弱化版本spoj687,没有字典序要求,直接输出mx值即可
复杂度就很稳了,O(n/1+n/2+...+n/n)=O(nlogn)
代码
#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
const int N=1e5+5;
int ans[N],cnt,ca;
namespace SA{
char s[N];
int n,m;
int *x,*y,X[N],Y[N],c[N],sa[N],height[N],Rank[N];
int dp[N][18],lg[N];
void get_sa(int _m=30){
m=_m;
x=X,y=Y;
for (int i=0;i<m;++i) c[i]=0;
for (int i=0;i<n;++i) x[i]=s[i]-'a',++c[x[i]];
for (int i=0;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 (sa[i]>=k) 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=0;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-1]]==y[sa[i]]&&((sa[i-1]+k<n?y[sa[i-1]+k]:-1)==(sa[i]+k<n?y[sa[i]+k]:-1))?p-1:p++;
if (p>n) break;
m=p;
}
}
void get_height(){
for (int i=0;i<n;++i) Rank[sa[i]]=i;
int k=0;height[0]=0;
for (int i=0;i<n;++i){
if (!Rank[i]) continue;
if (k) --k;
int j=sa[Rank[i]-1];
while (i+k<n&&j+k<n&&s[i+k]==s[j+k]) ++k;
height[Rank[i]]=k;
}
}
void get_Log(){
lg[0]=-1;
for(int i=1;i<N;++i){
lg[i]=lg[i>>1]+1;
}
}
void get_ST(){
for(int i=0;i<n;++i){
dp[i][0]=height[i];
}
for(int len=1;(1<<len)<=n;++len){
for(int l=0;l+(1<<len)-1<n;++l){
dp[l][len]=min(dp[l][len-1],dp[l+(1<<(len-1))][len-1]);
}
}
}
int rmq(int l,int r){
int k=lg[r-l+1];
return min(dp[l][k],dp[r-(1<<k)+1][k]);
}
int lcp(int l,int r){
l=Rank[l],r=Rank[r];
if(l==r)return n-sa[l];
if(l>r)swap(l,r);
return rmq(l+1,r);
}
//Rank[i]:下标位置在i的后缀的排名
//sa[i]:后缀排名第i的下标位置
//Rank和sa互为反函数 范围均在[0,n-1]
//height[i]:排名第i和排名第i-1的LCP长度
void PR(){
string p(s);
for(int i=0;i<n;++i)
printf("Rank[%d]:%d\n",i,Rank[i]);
for(int i=0;i<n;++i){
printf("sa[%d]:%d ",i,sa[i]);
cout<<p.substr(sa[i])<<endl;
}
for(int i=0;i<n;++i)
printf("height[%d]:%d\n",i,height[i]);
}
};
using namespace SA;
int main(){
get_Log();
while(~scanf("%s",s) && strcmp(s,"#")){
n=strlen(s);
get_sa();
get_height();
get_ST();
//PR();
int mx=1;
ans[cnt=1]=1;
for(int i=1;i<=n/2;++i){//枚举长度
for(int j=i;j<n;j+=i){
int x=lcp(j-i,j);
if(x<i)continue;//说明只出现了一次
int num=(i+x)/i;
if(x%i){
int off=i-x%i;//凑齐一个循环节
if(j-i-off>=0 && lcp(j-i-off,j-off)>=off){
num++;
}
}
if(num>mx){
mx=num;
ans[cnt=1]=i;//记录可行的长度
}
else if(num==mx){
if(i!=ans[cnt-1]){//长度去重
ans[++cnt]=i;
}
}
}
}
bool f=1;
for(int i=0;i<n && f;++i){//增序枚举rank 字典序最小
for(int j=1;j<=cnt && f;++j){
int len=ans[j];
if(lcp(sa[i],sa[i]+len)>=(mx-1)*len){
printf("Case %d: ",++ca);
for(int k=1;k<=mx;++k){
for(int l=sa[i];l<sa[i]+len;++l){
putchar(s[l]);
}
}
puts("");
f=0;
}
}
}
}
return 0;
}
字符串处理:寻找最长重复子串及其最小字典序
本文介绍了一种通过后缀数组和LCP计算找到字符串中出现次数最大且字典序最小的重复子串的方法,涉及构建后缀数组、高度数组和预处理操作,并提供了优化技巧。
456

被折叠的 条评论
为什么被折叠?



