传送门 - > (bzoj3670) 动物园
题目描述
近日,园长发现动物园中好吃懒做的动物越来越多了。例如企鹅,只会卖萌向游客要吃的。为了整治动物园的不良风气,让动物们凭自己的真才实学向游客要吃的,园长决定开设算法班,让动物们学习算法。
某天,园长给动物们讲解KMP算法。
园长:“对于一个字符串 S ,它的长度为 L 。我们可以在 O(L) 的时间内,求出一个名为next
的数组。有谁预习了next
数组的含义吗?”
熊猫:“对于字符串 S 的前 i 个字符构成的子串,既是它的后缀又是它的前缀的字符串中(它本身除外),最长的长度记作 next[i] 。”
园长:“非常好!那你能举个例子吗?”
熊猫:“例 S 为abcababc
,则 next[5]=2 。因为 SS 的前 55 个字符为abcab
,ab
既是它的后缀又是它的前缀,并且找不到一个更长的字符串满足这个性质。同理,还可得出 next[1]=next[2]=next[3]=0 , next[4] = next[6] = 1 , next[7]=2 , next[8] = 3 。”
园长表扬了认真预习的熊猫同学。随后,他详细讲解了如何在 O(L) 的时间内求出next
数组。
下课前,园长提出了一个问题:“KMP算法只能求出next
数组。我现在希望求出一个更强大num
数组一一对于字符串 S 的前 i 个字符构成的子串,既是它的后缀同时又是它的前缀,并且该后缀与该前缀不重叠,将这种字符串的数量记作 num[i] 。例如 S 为aaaaa
,则 num[4]=2 。这是因为 S 的前 4 个字符为aaaa
,其中a
和aa
都满足性质‘既是后缀又是前缀’,同时保证这个后缀与这个前缀不重叠。而aaa
虽然满足性质‘既是后缀又是前缀’,但遗憾的是这个后缀与这个前缀重叠了,所以不能计算在内。同理, num[1] = 0,num[2] = num[3] = 1,num[5] = 2。”
最后,园长给出了奖励条件,第一个做对的同学奖励巧克力一盒。听了这句话,睡了一节课的企鹅立刻就醒过来了!但企鹅并不会做这道题,于是向参观动物园的你寻求帮助。你能否帮助企鹅写一个程序求出 num 数组呢?
特别地,为了避免大量的输出,你不需要输出 num[i] 分别是多少,你只需要输出所有( num[i]+1 )的乘积,对 1,000,000,007 取模的结果即可。
输入输出格式
输入格式:
第 1 行仅包含一个正整数 n ,表示测试数据的组数。
随后 n 行,每行描述一组测试数据。每组测试数据仅含有一个字符串 S , S 的定义详见题目描述。数据保证 S中仅含小写字母。输入文件中不会包含多余的空行,行末不会存在多余的空格。
输出格式:
包含 n 行,每行描述一组测试数据的答案,答案的顺序应与输入数据的顺序保持一致。对于每组测试数据,仅需要输出一个整数,表示这组测试数据的答案对 1,000,000,007 取模的结果。输出文件中不应包含多余的空行。
输入输出样例
输入样例#1:
3
aaaaa
ab
abcababc
输出样例#1:
36
1
32
说明
测试点编号
约定
- N ≤ 5, L ≤ 50
- N ≤ 5, L ≤ 200
- N ≤ 5, L ≤ 200
- N ≤ 5, L ≤ 10,000
- N ≤ 5, L ≤ 10,000
- N ≤ 5, L ≤ 100,000
- N ≤ 5, L ≤ 200,000
- N ≤ 5, L ≤ 500,000
- N ≤ 5, L ≤ 1,000,000
- N ≤ 5, L ≤ 1,000,000
题解
这道题要求出前缀与后缀不重合的公共前后缀的数量,也就是长度小于等于原字符串长度一半的公共前后缀的数量,我们依然可以通过kmp的预处理,求出最长公共前后缀,然后暴力向前跳,直到跳到0,然后统计长度小于一半的数量,每次ans累乘,但是这样最坏是(O(n^2))的, 无法接受,洛谷上50分
50分Code
#include<cstdio>
#include<string>
#include<iostream>
#include<cstring>
#include<cmath>
#define Min(a,b) (a)<(b)?(a):(b)
#define Max(a,b) (a)>(b)?(a):(b)
#define in(i) (i=read())
using namespace std;
typedef long long lol;
int read() {
int ans=0,f=1; char i=getchar();
while(i<'0' || i>'9') {if(i=='-') f=-1; i=getchar();}
while(i>='0' && i<='9') {ans=(ans<<1)+(ans<<3)+i-'0'; i=getchar();}
return ans*f;
}
const int mod=1e9+7;
lol ans=1,n,sum;
int nex[1000010];
char s[1000010];
void get() {
int p=0;
for(int i=1;i<n;i++) {
while(p && s[p]!=s[i]) p=nex[p];
if(s[p]==s[i]) nex[i+1]=(++p);
else nex[i+1]=0;
}
}
void match() {
for(int i=n;i>=1;i--) {
int p=i,sum=0;
while(p) {
if(p<=(i>>1)) sum++;
p=nex[p];
}
if(nex[i]) ans=(ans*(sum+1)%mod)%mod;
}
}
int main()
{
int T; in(T);
while(T--) {
scanf("%s",s);
n=strlen(s);ans=1;
get(); match();
printf("%lld
",ans);
}
return 0;
}
那么我们想怎么优化呢,可不可以把((n^2))变成(O(nlogn))呢?显然是可以的,我们发现next[]数组是可以不断向前跳满足单调性的,所以可以使用倍增优化,(next[i][j])表示i的前(2^j)个公共前后缀的位置,同时(2 ^j)也表示有多少个合法数量,方便我们统计,那么代码应该很容易打出来了
80分Code
#include<cstdio>
#include<string>
#include<iostream>
#include<cstring>
#include<cmath>
#define in(i) (i=read())
using namespace std;
typedef long long lol;
int read() {
int ans=0,f=1; char i=getchar();
while(i<'0' || i>'9') {if(i=='-') f=-1; i=getchar();}
while(i>='0' && i<='9') {ans=(ans<<1)+(ans<<3)+i-'0'; i=getchar();}
return ans*f;
}
const int mod=1e9+7;
lol ans=1,n,sum;
int nex[1000010][20];
char s[1000010];
void get() {
int p=0;
for(int i=1;i<n;i++) {
while(p && s[p]!=s[i]) p=nex[p][0];
if(s[p]==s[i]) nex[i+1][0]=(++p);
else nex[i+1][0]=0;
}
}
void init() {
for(int j=1;j<=19;j++)
for(int i=n;i>=1;i--)
nex[i][j]=nex[nex[i][j-1]][j-1];
}
void match() {
for(int i=n;i>=1;i--) {
int p=i,sum=0;
for(int j=19;j>=0;j--)
if(nex[p][j]>(i>>1)) p=nex[p][j];
for(int k=19;k>=0;k--) {
if(nex[p][k]) p=nex[p][k],sum|=1<<k;
}
ans=(ans*(sum+1)%mod)%mod;
}
}
int main()
{
int T; in(T);
while(T--) {
scanf("%s",s);
n=strlen(s);ans=1;
get(); init(); match();
printf("%lld
",ans);
}
return 0;
}
这样就到了80分,然后听了大佬的玄学优化,把数组小的那一维放前面,方便取地址
80分/bzojAC Code
(bzoj - 5212ms)
#include<bits/stdc++.h>
#define in(i) (i=read())
using namespace std;
typedef long long lol;
int read() {
int ans=0,f=1; char i=getchar();
while(i<'0' || i>'9') {if(i=='-') f=-1; i=getchar();}
while(i>='0' && i<='9') {ans=(ans<<1)+(ans<<3)+i-'0'; i=getchar();}
return ans*f;
}
const int mod=1e9+7;
lol ans=1;
int n,sum;
int nex[20][1000010];
char s[1000010];
void get() {
int p=0;
for(int i=1;i<n;++i) {
while(p && s[p]!=s[i]) p=nex[0][p];
if(s[p]==s[i]) nex[0][i+1]=(++p);
else nex[0][i+1]=0;
}
}
void init() {
for(int j=1;j<=18;++j)
for(int i=n;i>=1;--i)
nex[j][i]=nex[j-1][nex[j-1][i]];
}
void match() {
for(int i=n;i>=1;--i) {
int p=i,sum=0;
for(int j=18;j>=0;--j)
if(nex[j][p]>(i>>1)) p=nex[j][p];
for(int k=18;k>=0;--k)
if(nex[k][p]) p=nex[k][p],sum|=1<<k;
ans=(ans*(sum+1)%mod)%mod;
}
}
int main()
{
int T; in(T);
while(T--) {
scanf("%s",s);
n=strlen(s);ans=1;
get(); init(); match();
printf("%lld
",ans);
}
return 0;
}
玄学玄学
其实还是可以优化,我们可以先预处理一个cnt[i]数组,代表到1~i有多少个公共前缀,很明显cnt[p]=cnt[next[p]]+1
先想最暴力的方法,从当前位置开始一直向前跳,但这样最坏复杂度是(O(n^2)),和暴力一样了
那么我们模拟kmp的匹配方法,接着上次位置往前跳,跳到第一个小于等于i的一半的地方,直接用cnt[p]去统计就可以了,这样近似复杂度(O(n))
跑的贼快
AC Code
(bzoj - 972ms)
#include<bits/stdc++.h>
#define in(i) (i=read())
using namespace std;
typedef long long lol;
int read() {
int ans=0,f=1; char i=getchar();
while(i<'0' || i>'9') {if(i=='-') f=-1; i=getchar();}
while(i>='0' && i<='9') {ans=(ans<<1)+(ans<<3)+i-'0'; i=getchar();}
return ans*f;
}
const int mod=1e9+7;
lol ans=1;
int n,sum;
int nex[1000010],cnt[1000010];
char s[1000010];
void get() {
int p=0;
for(int i=1;i<n;++i) {
while(p && s[p]!=s[i]) p=nex[p];
if(s[p]==s[i]) nex[i+1]=(++p);
else nex[i+1]=0;
}
}
void match() {
cnt[1]=1;
for(int i=2;i<=n;++i) cnt[i]=cnt[nex[i]]+1;
for(int i=0,p=0;i<n;++i) {
while(p && s[p]!=s[i]) p=nex[p];
p+=(s[p]==s[i]);
while(p>(i+1>>1)) p=nex[p];
ans=(ans*(cnt[p]+1)%mod)%mod;
}
}
int main()
{
int T; in(T);
while(T--) {
scanf("%s",s);
n=strlen(s);ans=1;
get(); match();
printf("%lld
",ans);
}
return 0;
}