【题目】NOI2014 动物园

本文深入探讨了KMP算法在处理字符串匹配问题时的next数组概念,并引入了一个优化后的num数组,用于统计字符串中既是后缀又是前缀但不重叠的子串数量。通过对next数组的巧妙运用,文章提出了一种O(n)时间复杂度的解决方案,避免了原始O(n^2)的高复杂度方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目大意

题目首先花了大量篇幅介绍了KMP算法。其中包括回退数组nextnextnext

  • 对于字符串SSS的前iii个字符构成的子串,既是它的后缀又是它的前缀的字符串中(它本身除外),最长的长度记作next[i]next[i]next[i]
  • 例如SSSabcababcabcababcabcababc,则next[5]=2next[5]=2next[5]=2。因为SSS的前555个字符为abcababcababcabababab既是它的后缀又是它的前缀,并且找不到一个更长的字符串满足这个性质。同理,还可得出next[1]=next[2]=next[3]=0next[1]=next[2]=next[3]=0next[1]=next[2]=next[3]=0next[4]=next[6]=1next[4]=next[6]=1next[4]=next[6]=1next[7]=2next[7]=2next[7]=2next[8]=3next[8]=3next[8]=3

而本题要求求出一个numnumnum数组:

  • 对于字符串SSS的前iii个字符构成的子串,既是它的后缀同时又是它的前缀,并且该后缀与该前缀不重叠,将这种字符串的数量记作num[i]num[i]num[i]
  • 例如SSSaaaaaaaaaaaaaaa,则num[4]=2num[4]=2num[4]=2。这是因为SSS的前444个字符为aaaaaaaaaaaa,其中aaaaaaaaa都满足性质“既是后缀又是前缀”,同时保证这个后缀与这个前缀不重叠。而aaaaaaaaa虽然满足性质“既是后缀又是前缀”,但是这个后缀与这个前缀重叠了,所以不能计算在内。同理,num[1]=0num[1]=0num[1]=0num[2]=num[3]=1num[2]=num[3]=1num[2]=num[3]=1num[5]=2num[5]=2num[5]=2

字符串的长度很大(L⩽1000000L\leqslant 1000000L1000000),结果输出∏i=1L(num[i]+1)mod  1000000007\prod\limits_{i=1}^L(num[i]+1)\mod 1000000007i=1L(num[i]+1)mod1000000007


思路

numnumnum数组中“后缀与前缀不重叠”的条件不容易处理,并且跟nextnextnext数组的定义大相径庭,故定义一个num′num'num数组:

  • 对于字符串SSS的前iii个字符构成的子串,既是它的后缀同时又是它的前缀的字符串的数量记作num[i]num[i]num[i]。这里的前缀和后缀包括SSS本身。

其实就是去掉“后缀与前缀不重叠”这个条件的numnumnum数组。

num′[i]num'[i]num[i]中统计的串中,首先一定有这个串本身;而其它的串都被囊括在next[i]next[i]next[i]表示的串内(包括两个长度为next[i]next[i]next[i]的串):


根据nextnextnext数组的定义,首尾两个长度为next[i]next[i]next[i]的串相同,把所有的串平移至同一个长度为nextnextnext的串内:

这些串就成了前next[i]next[i]next[i]个字符构成的子串中既是后缀又是前缀的字符串,根据定义,这些串的数量为num′[next[i]]num'[next[i]]num[next[i]]
再加上前iii个字符构成的子串本身,得出递推式:
num′[i]=num′[next[i]]+1num'[i]=num'[next[i]]+1num[i]=num[next[i]]+1

递推的时间复杂度是O(n)O(n)O(n)(这里以及下文的nnn指字符串长度LLL,也就是numnumnum数组的大小)。递推的边界是num′[0]=0num'[0]=0num[0]=0

现在考虑把“后缀与前缀不重叠”的条件加上。这个条件就是让num[i]num[i]num[i]中的串的长度不超过i2\frac i22i
为了让字符串既是后缀又是前缀,除了串本身以外,最长的字符串就是next[i]next[i]next[i]表示的串。若next[i]next[i]next[i]的长度已经不超过iii的一半,则num[i]num[i]num[i]就是num′[next[i]]num'[next[i]]num[next[i]]。若next[i]next[i]next[i]仍不满足要求,根据前面的平移操作和递推,下一个最长的串的长度就应该是next[next[i]]next[next[i]]next[next[i]],一直回退下去,直到串的长度为满足条件的最大的长度jjj。然后就有
num[i]=num′[j]num[i]=num'[j]num[i]=num[j]

不难得到下面的代码:

for(int i=1;i<=n;i++){
  int j=next[i];
  while(j*2>i)j=next[j];
  num[i]=num'[j];
}

然而这个做法的时间复杂度是O(n2)O(n^2)O(n2)。不妨试试样例的第一组数据:aaaaaaaaaaaaaaa
造成如此高的复杂度的原因是next[i]next[i]next[i]的规模是O(n)O(n)O(n)的,最坏情况下对于每个iiijjj都要减小O(n)O(n)O(n)次,然后就爆了。

考虑用KMP求nextnextnext数组的过程:

for(int i=2,j=0;i<=len;i++){
  while(j&&t[j+1]!=t[i])j=fail[j];
  if(t[j+1]==t[i])j++;fail[i]=j;
}

KMP的时间复杂度是O(n)O(n)O(n)的,因为iii111枚举到nnn,增加O(n)O(n)O(n)次;除了if语句中jjj增加O(n)O(n)O(n)次,其余时间jjj都在减少,所以jjj减少也不会超过O(n)O(n)O(n)次。

于是可以把上面的暴力修改成下面的代码:

for(int i=2,j=0;i<=len;i++){
  while(j&&t[j+1]!=t[i])j=fail[j];
  if(t[j+1]==t[i])j++;
  while(j*2>i)j=fail[j];
  num[i]=num'[j];
}

几个问题:

时间复杂度?

iii的枚举规模仍然是O(n)O(n)O(n)jjj还是在if语句处增加O(n)O(n)O(n)次,因此时间复杂度为O(n)O(n)O(n)

jjj是否是可行解(是否存在长度为jjj的串既是后缀又是前缀)?

jjj是由nextnextnext数组递推得到的,根据前面的递推,jjj是可行解。

jjj是否是最优解(jjj是否是不超过i2\frac i22i的最大可行解)?

由于jjj在当前的iii时要回退减小(第二个while语句),iii增加111时,jjj似乎就有可能不是最大的可行解。
根据前面得知,jjjnext[i]next[i]next[i]处回退一定不会错过最优解。若jjj不继续回退,得到的jjj就是next[i]next[i]next[i],不会错过最优解。现在jjj要继续回退,轮到下一个i′=i+1i'=i+1i=i+1时,为了当前子串的长度为j′j'j的前后缀匹配,jjj原本就要回退(第一个while语句)。

  • 若此次回退后j′j'j没有超过i′2\frac{i'}22i,就有j′⩽i+12j'\leqslant\frac{i+1}2j2i+1
    • iii为偶数时,j′⩽i2j'\leqslant\frac i2j2i,因此在这之前把jjj回退到i2\frac i22i就没有什么影响。
    • iii为奇数时,
      • j′⩽i−12j'\leqslant\frac{i-1}2j2i1,则在这之前把jjj回退到i2\frac i22i也没有什么影响。
      • j′=i+12j'=\frac{i+1}2j=2i+1,说明可以匹配的最长的前后缀刚好是子串的一半,根据KMP算法的原理,此时已匹配的长度j′j'j一定是由上一次的jjj111得来的。于是j=i−12j=\frac{i-1}2j=2i1,同样没有影响。
  • 若此次回退后j′j'j超过了i′2\frac{i'}22i,为了得到num[i′]num[i']num[i]的值,j′j'j仍然要回退,回到上一种情况。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值