【题解】[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;
}
posted @ 2021-02-23 09:40  zrkc  阅读(49)  评论(0)    收藏  举报