【题解】[NOI2014] 动物园
链接
关于 KMP 的题,除了一类关于匹配“状态”的,还有关于 公共前/后缀 的...
下面记录一下关于 公共前/后缀 的简单性质。
题意
给定字符串 \(S\) ,求一个 \(num\) 数组一一对于字符串 \(S\) 的前 \(i\) 个字符构成的子串,既是它的后缀同时又是它的前缀,并且该后缀与该前缀不重叠,将这种字符串的数量记作 \(num[i]\) 。
多组数据,分别输出 \(\prod (num[i]+1) \mod 1e9+7\)
样例
input #1
3
aaaaa
ab
abcababc
output #1
36
1
32
解法
我们来考虑 公共前/后缀 的性质:
回顾 失配函数 ,也常写作 \(next\) 数组:“对于字符串 \(S\) 的前 \(i\) 个字符构成的子串,既是它的后缀又是它的前缀的字符串中(它本身除外),最长的长度记作\(next[i]\) 。”我们至少先把 \(next\) 数组求出来。
对于一个前 \(i\) 个字符组成的子串,其顺次长度的 公共前/后缀 :显然最长的那个包含了其他全部的,且次大为最大的最大...故依次为:$$next[i],next[next[i]],next[next[next[i]]],...$$
而且注意到 \(next[i] < i\) ,所以这个状态机就是每个点不断往前跳的样子。
回到题,先考虑求前 \(i\) 个字符组成子串的所有公共前/后缀的数量 \(g[i]\) ,显然有 \(g[i] = g[next[i]]+1\) ,同时比划一下发现得让 \(g[0]=-1\)
然后如果能找到子串不大于一半长的最长的公共前/后缀 \(j\) ,对应就有 \(num[i] = g[j]+1\) 。
暴力往回跳,然后可以倍增优化,记 \(f[i][o]\) 为前 \(i\) 个字符组成子串第 \(2^o\) 长的公共前后缀,有 \(f[i][0] = next[i], f[i][o] = f[f[i][o-1]][o-1]\)
最坏复杂度 \(O(Tnlogn)\) ,因为不是最优的,一开始有两个点莽不过去。然后想到把常规的写法
for (register int o=1; o< 19; o++)
for (register int i=1; i<=N; i++) f[i][o] = f[f[i][o-1]][o-1];
改成
for (register int i=1; i<=N; i++)
for (register int o=1; o<=lg[g[i]]; o++) f[i][o] = f[f[i][o-1]][o-1];
只要某个幂已经达不到了,之后就不用算,这样就搞过了。
注意到这里考虑到了 \(next[i] < i\) 的性质,对于 \(i\) ,前面的都被计算过了。
#include <cstdio>
#include <cstring>
using namespace std;
typedef long long ll;
const int MAXN = 1000005;
const int mod = 1000000007;
int N, f[MAXN][25], g[MAXN], lg[MAXN]; char S[MAXN];
void work()
{
scanf("%s", S); N = strlen(S);
memset(f, 0, sizeof(f)), memset(g, 0, sizeof(g)); g[0] = -1;
for (register int i=1; i< N; i++) {
register int j = f[i][0];
while (j && S[i]!=S[j]) j = f[j][0];
f[i+1][0] = S[i]==S[j] ? j+1 : 0;
g[i+1] = g[f[i+1][0]] + 1;
}
for (register int i=1; i<=N; i++)
for (register int o=1; o<=lg[g[i]]; o++) f[i][o] = f[f[i][o-1]][o-1];
ll ans = 1;
for (register int i=1; i<=N; i++) {
register int j = i;
for (register int o=lg[g[i]]; o>=0; o--) if (f[j][o]>(i>>1)) j = f[j][o];
//printf("i = %d, j = %d\n", i, j);
if (f[j][0]<=(i>>1) && f[j][0]) ans = ans * (g[f[j][0]]+2) % mod;
}
printf("%lld\n", ans);
}
int main()
{
for (register int i=1; i< MAXN; i++) lg[i] = lg[i-1] + ((1<<(lg[i-1]+1))==i);
int T; for (scanf("%d", &T); T; T--) work();
}
分割线
好了,来看线性做法:
考虑上文“暴力往回跳”还能怎么做。
假设对于前 \(i\) 已经求得对应的位置 \(j\) ,我们还想继续利用这个 \(j\) 。
我们容易证明:对于前 \(i+1\) ,最大也只能是 \(j+1\) 。
假设有大于 \(j+1\) 的位置 \(k\) 满足,即 \(2k\) 不大于 \(i+1\) ,那么显然前 \(i\) 的子串有一个长为 \(k-1\) 的公共前/后缀符合题意,由假设知 \(k-1 > j\) ,这和 \(j\) 是最大的符合题意的矛盾。实际画个图就能意识到了。
\(j\) 是怎么变成 \(j+1\) 的呢?\(S[j]==S[i]\) 的时候,这还是看代码吧。
getfail(); // 先求 next
int j = 0; ll ans = 1;
for (register int i=1; i< N; i++) {
while (j && S[i]!=S[j]) j = f[j];
j = S[i]==S[j] ? j+1 : 0;
// while ((j<<1)>i+1) j = f[j]; // 不需要 while,最多跳一次就够了
if ((j<<1)>i+1) j = f[j];
if (j) ans = ans * (g[j]+2) % mod;
}

关于 失配函数 的东西
浙公网安备 33010602011771号