【字符串】字符串相关
啥都不会,重新学。先把模板摆上来,有时间写写例题。
1. KMP 算法
1.1. 模板
点击查看代码
//核心代码
int main(){
scanf("%s %s",s1+1,s2+1);
n=strlen(s1+1),m=strlen(s2+1);
//---------------------------------
for(int i=2,j=0;i<=m;++i){
while(j&&s2[j+1]!=s2[i])j=kmp[j];
if(s2[j+1]==s2[i])++j;
kmp[i]=j;
}
for(int i=1,j=0;i<=n;++i){
while(j&&s1[i]!=s2[j+1])j=kmp[j];
if(s1[i]==s2[j+1])++j;
if(j==m){printf("%d\n",i-m+1);j=kmp[j];}
}
//---------------------------------
for(int i=1;i<=m;++i)printf("%d ",kmp[i]);
return 0;
}
其中 表示模式串 的前缀 的最长相等真前缀和真后缀的长度,即有 ,。其原理就是通过自己匹配自己求出失配位置。
1.2 例题
P3435 [POI2006] OKR-Periods of Words
见“border 理论”。
2. Z 函数(exKMP)
2.1. 模板
点击查看代码
//z 函数构造方法
z[1]=m;
for(int i=2,l=0,r=0;i<=m;++i){
if(i<=r)z[i]=min(z[i-l+1],r-i+1);
while(i+z[i]<=m&&s[i+z[i]]==s[1+z[i]])++z[i];
if(i+z[i]-1>r)l=i,r=i+z[i]-1;
}
其中 表示字符串 与其后缀 的 LCP 长度,即有 。
其原理就是通过已匹配段 推出 ,其中 。
对于 ,直接上暴力即可。
对于 ,显然有 ,故令 (能保证在匹配段内正确)然后暴力匹配。
3. manacher 算法
3.1. 模板
点击查看代码
// 字符串通过插入分隔字符变成长为奇数
void get(){
s[1]='?';s[cnt=2]='#';register char c=getchar();
while(c<'a'||c>'z')c=getchar();
while(c>='a'&&c<='z')s[++cnt]=c,s[++cnt]='#',c=getchar();s[cnt+1]='!';
}
int main(){
get();
for(int i=2,l=0,r=-1;i<=cnt;++i){
d[i]=i>r?1:min(d[l+r-i],r-i+1);
while(s[i-d[i]]==s[i+d[i]])++d[i];
ans=max(d[i],ans);if(i+d[i]>r)r=i+d[i]-1,l=i-d[i]+1;
}
printf("%d",ans-1);
return 0;
}
其中 表示以 为中心的回文半径长度,即有 是回文串。
其构造思路跟上面差不多,考虑已匹配段 ,满足 是回文串。
对于 ,直接暴力。
对于 ,根据回文对称性,有以 为中心的回文串与 对称,则我们令 (长度超出匹配段的部分不能保证对称)然后暴力即可。
4. Suffix Array 后缀数组(SA)
4.1 模板
版本(直接排序)
char s[N];
int w,len,sa[N],rk[N<<1],oldrk[N<<1];//注意开双倍空间
inline bool cmp(int a,int b){
return rk[a]==rk[b]?rk[a+w]<rk[b+w]:rk[a]<rk[b];
}
int main(){
scanf("%s",s+1);len=strlen(s+1);
//------------------------------------------------
for(int i=1;i<=len;++i)sa[i]=i,rk[i]=s[i];
for(w=1;w<len;w<<=1){
sort(sa+1,sa+len+1,cmp);
memcpy(oldrk,rk,sizeof rk);
for(int i=1,p=0;i<=len;++i){
if(oldrk[sa[i]]==oldrk[sa[i-1]]&&oldrk[sa[i]+w]==oldrk[sa[i-1]+w])rk[sa[i]]=p;
else rk[sa[i]]=++p;
}
}
//------------------------------------------------
for(int i=1;i<=len;++i)printf("%d ",sa[i]);
return 0;
}
版本(基数排序优化)
const int N=1e6+10;
int n;
char s[N];
int buc[N],rk[N],ork[N],sa[N],id[N],pid[N];
//减少内存访问优化
bool cmp(int a,int b,int w){return ork[a]==ork[b]&&ork[a+w]==ork[b+w];}
int main(){
scanf("%s",s+1);n=strlen(s+1);
//------------------------------------------------
int m=1<<7,num=0;
for(int i=1;i<=n;++i)buc[rk[i]=s[i]]++;
for(int i=1;i<=m;++i)buc[i]+=buc[i-1];
for(int i=n;i;--i)sa[buc[rk[i]]--]=i;//由于要稳定排序故倒序枚举
for(int w=1;;w<<=1,m=num,num=0){
//对第二关键字排序优化
for(int i=n-w+1;i<=n;++i)id[++num]=i;//i+w>n的话一定排在前面(第二关键字为空)
for(int i=1;i<=n;++i)if(sa[i]>w)id[++num]=sa[i]-w;//这里枚举的sa[i]实际上是sa[i+w]即第二关键字
//对第一关键字排序
for(int i=1;i<=m;++i)buc[i]=0;
for(int i=1;i<=n;++i)buc[pid[i]=rk[id[i]]]++;//用pid代替,使访问连续
for(int i=1;i<=m;++i)buc[i]+=buc[i-1];
for(int i=n;i;--i)sa[buc[pid[i]]--]=id[i];//稳定排序,如果第一关键字相同则按照原来排好的第二关键字顺序
swap(rk,ork);num=0;
for(int i=1;i<=n;++i)rk[sa[i]]=cmp(sa[i-1],sa[i],w)?num:++num;//离散化
if(num==n)break;//排好就退出
}
//------------------------------------------------
for(int i=1;i<=n;++i)printf("%d ",sa[i]);
return 0;
}
后缀排序里主要用到两个数组:
-
表示排名为 的后缀编号。
-
表示编号为 的后缀排名。
于是就有 (禁止套娃)。OI-wiki 里面有个很好的图:
后缀排序的过程运用了倍增的思想:
假设我们当前知道了每个长为 的子串的排名,记为 。
则我们可以通过 和 双关键字排序,求出 (若 则规定 为负无穷,它一定排在前面)。OI-wiki 又有个很好的图:
直接双关键字排序是 的。注意到 的值域是 的,可以用基数排序优化至 。
4.2 height 数组
可以说 SA 算法的目的就是求出 height 数组。下面给出几个定义和性质:
定义:
-
:后缀
-
:排名为 的后缀与排名为 的后缀的最长公共前缀,即 。
-
:。
性质:
-
-
-
()
这个需要证明一下(写的有点不太严谨感性理解):
首先根据后缀排序的定义, 和 一段相同长度的前缀 ,总有 (由字典序比较可知), 同理。
设 为图中橙色一段前缀, 为图中蓝色一段前缀。
则图中 橙色的一段前缀也一定与 的前缀相等,于是我们证明了 。
设 为图中 橙色的一段前缀,对于 的一段相同长度的前缀,总有 ,又有 ,故 。
综上得证。
于是可知,。
但是 数组怎么求呢?有一个关键的性质:,再乱证一下:
假设 ,令 。
当 与 的第一个字母不同,则 ,上述结论显然成立。
当 与 的第一个字母相同,则我们可以去掉它们的第一个字母,那么 和 就变成了 和 。
显然 。又有 ,即 。
于是我们可以写出下面构造 数组的代码。
点击查看代码
for(int i=1,k=0;i<=n;++i){
if(k)--k;
int j=sa[rk[i]-1];
while(s[i+k]==s[j+k])++k;
ht[rk[i]]=k;
}
注意到 指针最多移动 次,故时间复杂度 。
4.3 例题:
P2178 [NOI2015] 品酒大会
“子串相等”可以转化为“后缀的前缀相等”,于是可以用 height 数组来做。
考虑排序后的后缀,当前要求 相似的子串。我们发现,对于 ,我们可以把 和 划分为两个连通块,同一块内的元素都是可以组合的,因为它们的 都不小于 。再者我们发现,随着 增大,连通块的个数不断减小,当 时,每个元素自成一个连通块。于是我们考虑倒着求答案,每次把 的两个联通块合并,这个可以用并查集实现。
注意权值有正有负,要维护块内最大最小次大次小。
点击查看代码
#define pb push_back
typedef long long ll;
typedef pair<ll,ll> pr;
const int N=3e5+10;
const ll inf=1e18;
int n;char s[N];
int sa[N],rk[N],ork[N],buc[N],id[N],pid[N],ht[N];
inline bool cmp(int a,int b,int w){return ork[a]==ork[b]&&ork[a+w]==ork[b+w];}
void build(){
int m=1<<7,p=0;
for(int i=1;i<=n;++i)buc[rk[i]=s[i]]++;
for(int i=1;i<=m;++i)buc[i]+=buc[i-1];
for(int i=n;i;--i)sa[buc[rk[i]]--]=i;
for(int w=1;;w<<=1,m=p,p=0){
for(int i=n-w+1;i<=n;++i)id[++p]=i;
for(int i=1;i<=n;++i)if(sa[i]>w)id[++p]=sa[i]-w;
for(int i=1;i<=m;++i)buc[i]=0;
for(int i=1;i<=n;++i)buc[pid[i]=rk[id[i]]]++;
for(int i=1;i<=m;++i)buc[i]+=buc[i-1];
for(int i=n;i;--i)sa[buc[pid[i]]--]=id[i];
swap(ork,rk);p=0;
for(int i=1;i<=n;++i)rk[sa[i]]=cmp(sa[i],sa[i-1],w)?p:++p;
if(p==n)break;
}
for(int i=1,k=0;i<=n;++i){
if(k)--k;
int j=sa[rk[i]-1];
while(s[i+k]==s[j+k])++k;
ht[rk[i]]=k;
}
}
vector<int> h[N];
ll a[N],fa[N],siz[N],mx1[N],mx2[N],mn1[N],mn2[N];
ll res=0,maxv=-inf;
pr ans[N];
int fnd(int x){return x!=fa[x]?fa[x]=fnd(fa[x]):x;}
inline ll get(int x){return 1ll*x*(x-1)/2;}
pr calc(int v){
for(int x:h[v]){
int l=fnd(x-1),r=fnd(x);
if(siz[l]>siz[r])swap(l,r);
res-=get(siz[l])+get(siz[r]);
siz[r]+=siz[l],fa[l]=r;
res+=get(siz[r]);
if(mx1[l]>=mx1[r]){
mx2[r]=max(mx1[r],mx2[l]);
mx1[r]=mx1[l];
}else if(mx1[l]>mx2[r])mx2[r]=mx1[l];
if(mn1[l]<=mn1[r]){
mn2[r]=min(mn1[r],mn2[l]);
mn1[r]=mn1[l];
}else if(mn1[l]<mn2[r])mn2[r]=mn1[l];
maxv=max(maxv,max(mx1[r]*mx2[r],mn1[r]*mn2[r]));
}
return maxv==-inf?make_pair(0ll,0ll):make_pair(res,maxv);
}
int main(){
scanf("%d%s",&n,s+1);
for(int i=1;i<=n;++i)scanf("%lld",&a[i]);
build();
for(int i=1;i<=n;++i){
fa[i]=i,siz[i]=1;
mx1[i]=mn1[i]=a[sa[i]];
mx2[i]=-inf,mn2[i]=inf;
}
for(int i=2;i<=n;++i)h[ht[i]].pb(i);
for(int i=n-1;~i;--i)ans[i]=calc(i);
for(int i=0;i<n;++i)printf("%lld %lld\n",ans[i].first,ans[i].second);
return 0;
}
P4070 [SDOI2016]生成魔咒
对每个前缀求本质不同子串个数。
先考虑对整个字符串的本质不同子串个数怎么求,有结论 ,证明一下:
设 排在第一位,则显然它的每个前缀在前面都没出现过,贡献为 。
对于 ,它在前面出现过的前缀个数即为 ,即为图中橙色部分,贡献为 。
对于 ,它在前面出现过的前缀个数为 ,即为图中蓝线部分,之后的前缀在前面一定没出现过。考虑反证法:若之后的前缀在前面有出现过(设为图中绿色部分),则有 绿色部分长度。但是根据性质 又有 ,矛盾,故原命题正确。则贡献为 。
以此类推,总贡献为 。
再来考虑这道题怎么做,每在后面增加一个字符,则会影响所有后缀。我们不妨把整个串翻转过来,变为在前面加入字符,于是每次操作只会增加一个后缀而不影响其他的后缀,且贡献不变。加字符的操作变成减字符的操作更好做,每次合并 和 ,根据性质 可求得新的贡献。那么维护一个双链表就可以了。
点击查看代码
const int N=1e5+10;
typedef long long ll;
int n,tot,s[N],tmp[N];
int sa[N],rk[N],ork[N],buc[N],id[N],pid[N],ht[N];
int pre[N],nxt[N];
ll ans[N],sum=0;
inline bool cmp(int a,int b,int w){return ork[a]==ork[b]&&ork[a+w]==ork[b+w];}
void build(){
int m=tot,p=0;
for(int i=1;i<=n;++i)buc[rk[i]=s[i]]++;
for(int i=1;i<=m;++i)buc[i]+=buc[i-1];
for(int i=n;i;--i)sa[buc[rk[i]]--]=i;
for(int w=1;;w<<=1,m=p,p=0){
for(int i=n-w+1;i<=n;++i)id[++p]=i;
for(int i=1;i<=n;++i)if(sa[i]>w)id[++p]=sa[i]-w;
for(int i=1;i<=m;++i)buc[i]=0;
for(int i=1;i<=n;++i)buc[pid[i]=rk[id[i]]]++;
for(int i=1;i<=m;++i)buc[i]+=buc[i-1];
for(int i=n;i;--i)sa[buc[pid[i]]--]=id[i];
swap(ork,rk);p=0;
for(int i=1;i<=n;++i)rk[sa[i]]=cmp(sa[i],sa[i-1],w)?p:++p;
if(p==n)break;
}
for(int i=1,k=0;i<=n;++i){
if(k)--k;
int j=sa[rk[i]-1];
while(s[i+k]==s[j+k])++k;
ht[rk[i]]=k;
}
}
int main(){
read(n);
for(int i=n;i;--i)read(s[i]),tmp[i]=s[i];
sort(tmp+1,tmp+n+1);tot=unique(tmp+1,tmp+n+1)-tmp-1;
for(int i=1;i<=n;++i)s[i]=lower_bound(tmp+1,tmp+tot+1,s[i])-tmp;
build();
for(int i=1;i<=n;++i){
sum+=n-sa[i]+1-ht[i];
pre[i]=i-1,nxt[i]=i+1;
}nxt[0]=1,pre[n+1]=n;ans[n]=sum;
for(int i=1;i<n;++i){
int k=rk[i],j=nxt[k];
sum-=n-i+1-ht[k];sum+=ht[j];
ht[j]=min(ht[k],ht[j]);
pre[j]=pre[k],nxt[pre[k]]=j;
sum-=ht[j];
ans[n-i]=sum;
}
for(int i=1;i<=n;++i)printf("%lld\n",ans[i]);
return 0;
}
CF822E Liar
显然可以贪心,尽量让极长的一段来匹配。注意到 较小,可以 dp。
设 表示从 中选取不超过 个子串,最多能匹配到 的第几位。
若当前不选,则转移到 ;若当前选择,则要选择极长的一段,即 和 那一段后缀的 ,这个可以通过把 合并成一个字符串,然后用 st 表维护区间 即可。设 为 ,则转移到 。
点击查看代码
const int N=2e5+10;
int n,lens,lent,tot=0,x;
char s[N];
int sa[N],rk[N],ork[N],buc[N],id[N],pid[N],ht[N];
int lg[N],st[N][18],f[31][N];
inline bool cmp(int a,int b,int w){return ork[a]==ork[b]&&ork[a+w]==ork[b+w];}
void build(){
int m=1<<7,p=0;n=tot;
for(int i=1;i<=n;++i)buc[rk[i]=s[i]]++;
for(int i=1;i<=m;++i)buc[i]+=buc[i-1];
for(int i=n;i;--i)sa[buc[rk[i]]--]=i;
for(int w=1;;w<<=1,m=p,p=0){
for(int i=n-w+1;i<=n;++i)id[++p]=i;
for(int i=1;i<=n;++i)if(sa[i]>w)id[++p]=sa[i]-w;
for(int i=1;i<=m;++i)buc[i]=0;
for(int i=1;i<=n;++i)buc[pid[i]=rk[id[i]]]++;
for(int i=1;i<=m;++i)buc[i]+=buc[i-1];
for(int i=n;i;--i)sa[buc[pid[i]]--]=id[i];
swap(ork,rk),p=0;
for(int i=1;i<=n;++i)rk[sa[i]]=cmp(sa[i],sa[i-1],w)?p:++p;
if(p==n)break;
}
for(int i=1,k=0;i<=n;++i){
if(k)--k;
int j=sa[rk[i]-1];
while(s[i+k]==s[j+k])++k;
ht[rk[i]]=k;
}
for(int i=1;i<=n;++i)st[i][0]=ht[i];
for(int i=2;i<=n;++i)lg[i]=lg[i>>1]+1;
for(int j=1;j<=lg[n];++j){
for(int i=1;i+(1<<j)-1<=n;++i){
st[i][j]=min(st[i][j-1],st[i+(1<<(j-1))][j-1]);
}
}
}
int lcp(int i,int j){
if(i>lens||j>lent)return 0;
i=rk[i],j=rk[j+lens+1];
if(i>j)swap(i,j);++i;
int p=lg[j-i+1];
return min(st[i][p],st[j-(1<<p)+1][p]);
}
int main(){
scanf("%d",&lens);
for(int i=1;i<=lens;++i)scanf(" %c",&s[++tot]);s[++tot]='#';
scanf("%d",&lent);
for(int i=1;i<=lent;++i)scanf(" %c",&s[++tot]);
build();scanf("%d",&x);
for(int i=1;i<=x;++i){
for(int j=0;j<lens;++j){
int L=lcp(j+1,f[i-1][j]+1);
f[i][j+1]=max(f[i][j+1],f[i][j]);
f[i][j+L]=max(f[i][j+L],f[i-1][j]+L);
}
}
if(f[x][lens]==lent)printf("YES\n");
else printf("NO\n");
return 0;
}
P4248 [AHOI2013]差异
原式 = 。左半部分可以先不管他,考虑怎么求右半部分,他显然可以化为:
这事实上是个很典的做法:考虑枚举 作为最小值,求出它向左和向右最多能扩展到哪里,计算一下经过它的贡献即可。这个过程可以用单调栈维护。
点击查看代码
const int N=5e5+10;
#define int long long
int n;char s[N];
int sa[N],rk[N],ork[N],buc[N],id[N],pid[N],ht[N];
inline bool cmp(int a,int b,int w){return ork[a]==ork[b]&&ork[a+w]==ork[b+w];}
void build(){
int m=1<<7,p=0;
for(int i=1;i<=n;++i)buc[rk[i]=s[i]]++;
for(int i=1;i<=m;++i)buc[i]+=buc[i-1];
for(int i=n;i;--i)sa[buc[rk[i]]--]=i;
for(int w=1;;w<<=1,m=p,p=0){
for(int i=n-w+1;i<=n;++i)id[++p]=i;
for(int i=1;i<=n;++i)if(sa[i]>w)id[++p]=sa[i]-w;
for(int i=1;i<=m;++i)buc[i]=0;
for(int i=1;i<=n;++i)buc[pid[i]=rk[id[i]]]++;
for(int i=1;i<=m;++i)buc[i]+=buc[i-1];
for(int i=n;i;--i)sa[buc[pid[i]]--]=id[i];
swap(ork,rk);p=0;
for(int i=1;i<=n;++i)rk[sa[i]]=cmp(sa[i],sa[i-1],w)?p:++p;
if(p==n)break;
}
for(int i=1,k=0;i<=n;++i){
if(k)--k;
int j=sa[rk[i]-1];
while(s[i+k]==s[j+k])++k;
ht[rk[i]]=k;
}
}
int stk[N],top=0;
int l[N],r[N];
signed main(){
scanf("%s",s+1);n=strlen(s+1);
build();
int ans=1ll*n*(n-1)*(n+1)/2;
for(int i=1;i<=n;++i){
while(top&&ht[stk[top]]>ht[i])r[stk[top--]]=i;
l[i]=stk[top],stk[++top]=i;
}
while(top)r[stk[top--]]=n+1;
for(int i=2;i<=n;++i)ans-=2ll*(i-l[i])*(r[i]-i)*ht[i];
printf("%lld\n",ans);
return 0;
}
P3975 [TJOI2015]弦论
几道例题里唯一一道自己口胡出来对的题(
对于 的询问很 naive,排名为 的串与前面的本质不同子串个数就是 ,枚举一下就行了。
对于 的询问有点复杂,考虑一个串的排名是啥:
因为不用考虑相同子串,故排名在 前的后缀构成的子串数量就是 。但是还要考虑排名在它后面的后缀,它们构成的子串字典序比 小的个数就是它和 的 。故 的排名就是:
这是显然可以二分的,我们要找的目标后缀就是排名最大且小于 的那个后缀。现在的问题变成要输出的长度为多少,这个简单。设当前枚举的长度为 ,考虑排名在它后面的与它存在长度为 的公共前缀的后缀个数(即 ,记得还要加上自己本身),这也是可二分的,如果加上这个个数后排名大于 则退出即可。
总共复杂度是 的,比 SAM 做法稍劣。
点击查看代码
const int N=5e5+10;
#define int long long
int n,t,k;char s[N];
int sa[N],rk[N],ork[N],buc[N],id[N],pid[N],ht[N];
int st[20][N],lg[N];
inline bool cmp(int a,int b,int w){return ork[a]==ork[b]&&ork[a+w]==ork[b+w];}
void build(){
int m=1<<7,p=0;
for(int i=1;i<=n;++i)buc[rk[i]=s[i]]++;
for(int i=1;i<=m;++i)buc[i]+=buc[i-1];
for(int i=n;i;--i)sa[buc[rk[i]]--]=i;
for(int w=1;;w<<=1,m=p,p=0){
for(int i=n-w+1;i<=n;++i)id[++p]=i;
for(int i=1;i<=n;++i)if(sa[i]>w)id[++p]=sa[i]-w;
for(int i=1;i<=m;++i)buc[i]=0;
for(int i=1;i<=n;++i)buc[pid[i]=rk[id[i]]]++;
for(int i=1;i<=m;++i)buc[i]+=buc[i-1];
for(int i=n;i;--i)sa[buc[pid[i]]--]=id[i];
swap(ork,rk);p=0;
for(int i=1;i<=n;++i)rk[sa[i]]=cmp(sa[i],sa[i-1],w)?p:++p;
if(p==n)break;
}
for(int i=1,k=0;i<=n;++i){
if(k)--k;
int j=sa[rk[i]-1];
while(s[i+k]==s[j+k])++k;
ht[rk[i]]=k;
}
lg[0]=-1;
for(int i=1;i<=n;++i)st[0][i]=ht[i],lg[i]=lg[i>>1]+1;
for(int i=1;i<=lg[n];++i){
for(int j=1;j+(1<<i)-1<=n;++j){
st[i][j]=min(st[i-1][j],st[i-1][j+(1<<(i-1))]);
}
}
}
int query(int l,int r){
int p=lg[r-l+1];
return min(st[p][l],st[p][r-(1<<p)+1]);
}
bool check(int mid){
int sum=0;
for(int i=1;i<mid;++i)sum+=n-sa[i]+1;
for(int i=mid+1;i<=n;++i)sum+=query(mid+1,i);
return sum+1>=k;
}
int fnd(int p,int l,int r,int w){
int mid,res=p;
while(l<=r){
mid=(l+r)>>1;
if(query(p+1,mid)>=w)res=mid,l=mid+1;
else r=mid-1;
}return res-p;
}
signed main(){
scanf("%s%lld%lld",s+1,&t,&k),n=strlen(s+1);
build();
int tot=0;
if(!t){
for(int i=1;i<=n;++i){
int len=n-sa[i]+1-ht[i];
if(tot+len>=k){
int pos=sa[i];k-=tot-ht[i];
while(k--)printf("%c",s[pos++]);
return 0;
}tot+=len;
}printf("-1\n");
}else {
if(k>n*(n+1)/2)return printf("-1\n"),0;
int l=1,r=n,mid;
while(l<r){
int mid=(l+r)>>1;
if(check(mid))r=mid;
else l=mid+1;
}--r;
int len=n-sa[r]+1,tot=0;
for(int i=1;i<r;++i)tot+=n-sa[i]+1;
for(int i=1;i<=len;++i){
int cnt=fnd(r,r+1,n,i);
printf("%c",s[sa[r]+i-1]);tot+=cnt+1;
if(tot>=k)break;
}
}
return 0;
}
5. border 理论
5.1 相关定理
剽窃自/证明见:cmd 传送门
记 。
。
即, 的所有 等于最大 加上最大 的所有 。kmp 里的 fail 数组求的就是最大 。
若 为 的周期,且 ,则 也是 的周期。
若 是 的前缀,且 有周期 , 有整周期 ,满足 ,,则 也有周期 。
若 如下图匹配,则表示 有长度为 的周期,也即有长度为 的 。
若 ,则 在 中的匹配位置必为等差序列。
的长度大于 的 长度构成一个等差序列。
一个串 的所有 按长度排序后,可以被划分成 个等差序列。
5.2 例题
P3435 [POI2006] OKR-Periods of Words
对于满足条件的 , 与 相差的部分就是 的 ,我们要求最大周期长度就是求最小非空 。
考虑对于每个前缀暴力跳 数组(kmp 那个),跳到不能再跳的 即为所求。考虑优化一下,对于前缀 ,若它的最小非空 存在,我们可以直接将 设成它,可以缩短路径。
点击查看代码
const int N=1e6+10;
int n;char s[N];
int fail[N];
int main(){
scanf("%d%s",&n,s+1);
for(int i=2,j=0;i<=n;++i){
while(j&&s[i]!=s[j+1])j=fail[j];
fail[i]=s[i]==s[j+1]?++j:j;
}long long ans=0;
for(int i=1;i<=n;++i){
int j=i;while(fail[j])j=fail[j];
ans+=i-j;
if(fail[i])fail[i]=j;
}printf("%lld\n",ans);
return 0;
}
P2375 [NOI2014] 动物园
显然有个暴力的方法:对每个前缀不断跳 ,知道其长度小于等于前缀长度的一半,那么 就等于它还能继续跳的次数,这个可以在建 时递推出。考虑怎么优化:
类似构建 数组的过程,我们可以在 上匹配自身,然后跑上面的暴力。由于我们减少了重复递归,可以证明是均摊 的。
点击查看代码
const int N=1e6+10;
typedef long long ll;
const ll mod=1e9+7;
int n;char s[N];
int fail[N];
ll cnt[N],ans=1;
void solve(){
scanf("%s",s+1);n=strlen(s+1);
memset(fail,0,sizeof fail);cnt[0]=0,cnt[1]=1;
for(int i=2,j=0;i<=n;++i){
while(j&&s[j+1]!=s[i])j=fail[j];
j+=(s[j+1]==s[i]);
fail[i]=j;cnt[i]=cnt[j]+1;
}ans=1;
for(int i=2,j=0;i<=n;++i){
while(j&&s[j+1]!=s[i])j=fail[j];
j+=(s[j+1]==s[i]);
while((j<<1)>i)j=fail[j];
ans=ans*(cnt[j]+1)%mod;
}printf("%d\n",ans);
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探