【题目】NOI2014 动物园

题目大意

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

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

而本题要求求出一个 n u m num num数组:

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

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


思路

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

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

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

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


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

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

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

现在考虑把“后缀与前缀不重叠”的条件加上。这个条件就是让 n u m [ i ] num[i] num[i]中的串的长度不超过 i 2 \frac i2 2i
为了让字符串既是后缀又是前缀,除了串本身以外,最长的字符串就是 n e x t [ i ] next[i] next[i]表示的串。若 n e x t [ i ] next[i] next[i]的长度已经不超过 i i i的一半,则 n u m [ i ] num[i] num[i]就是 n u m ′ [ n e x t [ i ] ] num'[next[i]] num[next[i]]。若 n e x t [ i ] next[i] next[i]仍不满足要求,根据前面的平移操作和递推,下一个最长的串的长度就应该是 n e x t [ n e x t [ i ] ] next[next[i]] next[next[i]],一直回退下去,直到串的长度为满足条件的最大的长度 j j j。然后就有
n u m [ i ] = n u m ′ [ 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 ( n 2 ) O(n^2) O(n2)。不妨试试样例的第一组数据: a a a a a aaaaa aaaaa
造成如此高的复杂度的原因是 n e x t [ i ] next[i] next[i]的规模是 O ( n ) O(n) O(n)的,最坏情况下对于每个 i i i j j j都要减小 O ( n ) O(n) O(n)次,然后就爆了。

考虑用KMP求 n e x t next next数组的过程:

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)的,因为 i i i 1 1 1枚举到 n n n,增加 O ( n ) O(n) O(n)次;除了if语句中 j j j增加 O ( n ) O(n) O(n)次,其余时间 j j j都在减少,所以 j j j减少也不会超过 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];
}

几个问题:

时间复杂度?

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

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

j j j是由 n e x t next next数组递推得到的,根据前面的递推, j j j是可行解。

j j j是否是最优解( j j j是否是不超过 i 2 \frac i2 2i的最大可行解)?

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值