题目大意
题目首先花了大量篇幅介绍了KMP算法。其中包括回退数组 :
- 对于字符串 的前 个字符构成的子串,既是它的后缀又是它的前缀的字符串中(它本身除外),最长的长度记作 。
- 例如 为 ,则 。因为 的前 个字符为 , 既是它的后缀又是它的前缀,并且找不到一个更长的字符串满足这个性质。同理,还可得出 , , , 。
而本题要求求出一个 数组:
- 对于字符串 的前 个字符构成的子串,既是它的后缀同时又是它的前缀,并且该后缀与该前缀不重叠,将这种字符串的数量记作 。
- 例如 为 ,则 。这是因为 的前 个字符为 ,其中 和 都满足性质“既是后缀又是前缀”,同时保证这个后缀与这个前缀不重叠。而 虽然满足性质“既是后缀又是前缀”,但是这个后缀与这个前缀重叠了,所以不能计算在内。同理, , , 。
字符串的长度很大( ),结果输出 。
思路
数组中“后缀与前缀不重叠”的条件不容易处理,并且跟 数组的定义大相径庭,故定义一个 数组:
- 对于字符串 的前 个字符构成的子串,既是它的后缀同时又是它的前缀的字符串的数量记作 。这里的前缀和后缀包括 本身。
其实就是去掉“后缀与前缀不重叠”这个条件的 数组。
中统计的串中,首先一定有这个串本身;而其它的串都被囊括在 表示的串内(包括两个长度为 的串):
根据
数组的定义,首尾两个长度为
的串相同,把所有的串平移至同一个长度为
的串内:
这些串就成了前
个字符构成的子串中既是后缀又是前缀的字符串,根据定义,这些串的数量为
。
再加上前
个字符构成的子串本身,得出递推式:
递推的时间复杂度是 (这里以及下文的 指字符串长度 ,也就是 数组的大小)。递推的边界是 。
现在考虑把“后缀与前缀不重叠”的条件加上。这个条件就是让
中的串的长度不超过
。
为了让字符串既是后缀又是前缀,除了串本身以外,最长的字符串就是
表示的串。若
的长度已经不超过
的一半,则
就是
。若
仍不满足要求,根据前面的平移操作和递推,下一个最长的串的长度就应该是
,一直回退下去,直到串的长度为满足条件的最大的长度
。然后就有
不难得到下面的代码:
for(int i=1;i<=n;i++){
int j=next[i];
while(j*2>i)j=next[j];
num[i]=num'[j];
}
然而这个做法的时间复杂度是
。不妨试试样例的第一组数据:
。
造成如此高的复杂度的原因是
的规模是
的,最坏情况下对于每个
,
都要减小
次,然后就爆了。
考虑用KMP求 数组的过程:
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的时间复杂度是
的,因为
从
枚举到
,增加
次;除了if
语句中
增加
次,其余时间
都在减少,所以
减少也不会超过
次。
于是可以把上面的暴力修改成下面的代码:
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];
}
几个问题:
时间复杂度?
的枚举规模仍然是
,
还是在if
语句处增加
次,因此时间复杂度为
。
是否是可行解(是否存在长度为 的串既是后缀又是前缀)?
是由 数组递推得到的,根据前面的递推, 是可行解。
是否是最优解( 是否是不超过 的最大可行解)?
由于
在当前的
时要回退减小(第二个while
语句),
增加
时,
似乎就有可能不是最大的可行解。
根据前面得知,
从
处回退一定不会错过最优解。若
不继续回退,得到的
就是
,不会错过最优解。现在
要继续回退,轮到下一个
时,为了当前子串的长度为
的前后缀匹配,
原本就要回退(第一个while
语句)。
- 若此次回退后
没有超过
,就有
。
- 为偶数时, ,因此在这之前把 回退到 就没有什么影响。
-
为奇数时,
- 若 ,则在这之前把 回退到 也没有什么影响。
- 若 ,说明可以匹配的最长的前后缀刚好是子串的一半,根据KMP算法的原理,此时已匹配的长度 一定是由上一次的 加 得来的。于是 ,同样没有影响。
- 若此次回退后 超过了 ,为了得到 的值, 仍然要回退,回到上一种情况。