循环节与最短循环节:
若某个字符串是由某个子串循环构成的,那么就称该子串为原串的循环节,长度最短的循环节就是最短循环节。
如abababab,abab和ab都是原串的循环节,而最短循环节是ab。
结论:
如果字符串 s 有个循环节 son,n = |s| , x = |son|,字符数组下标从1开始,那么:
- x 一定是 n 的约数。
- 那么s[1,n-x] = s[x, n] = son。
证明:
结论1:如果 x 不是 n 的约数,那么自然 n 就不可能由 若干个 x 相加构成,即不满足 n = k * x ,其中 k 是正整数。
结论2:若想s[1 ,n-x] = s[x, n] ,必须s[1, x] = s[ x ,2x] = s[2x , 3x] = … = s[n-x , n],刚好符合循环节定义,因此 结论2 成立。
求单个字符串的最短循环节
例1: POJ 2406 Power Strings (KMP)
测试地址
题意简述: 给定一个字符串 s ,求出它最多由某个子串 循环几次 构成。
解题思路:
首先 s 必须是由某个循环节循环若干次构成的,否则无解。题意让求最多循环几次构成,那么既然 s 的长度固定是 n,自然是循环节越短,那么循环次数越多了。
我们先求出KMP算法中的nex数组。
结论3: 此时若 s 有解,那么最短循环节长度为 x = n - nex[n] 。
证明:反证法
- 首先根据结论2,因为 s[1,nex[n] ] = s[n - nex[n] , n],所以必然存在长度为 x = n - nex[n]的循环节,问题就在于它是不是最短的。
- 假设存在循环节,长度为 y(y < x) ,那么根据结论2,一定有: s[1, n-y] = s[y , n];如此一来nex[n] = n-y > n-x,这和nex数组定义矛盾,因此不存在y < x。
- 综上所述,n - nex[n] 一定为最短循环节的长度。
因此本题的答案就是 n/(n-nex[n]),若不能整除,则无解。
代码示例: 见附录部分 code-1:Power Strings
求任意子串的最短循环节
例2: bzoj2795 Horrible Poem
测试地址
题意简述:
给出一个由小写英文字母组成的字符串 S,再给出 q 个询问,要求回答 S 某个子串的最短循环节长度。如果字符串 B 是字符串 A 的循环节,那么 A 可以由 B 重复若干次得到。
解题思路:
此题的 q 很大,我们不可能对每一个子串都O(n)求出 nex 数组再回答。我们利用 滚动哈希 ,在O(1)时间内利用 结论2 判断某个长度是否为循环节。
再根据结论1,可以得知循环节长度一定是子串长度 m 的约数,因此我们
O
(
m
)
O(\sqrt m)
O(m)分解约数,再用O(1)复杂度用hash判断,本题总复杂度就是
O
(
q
m
)
O(q \sqrt m)
O(qm)。
一般到这里就该结束了,我们利用了 结论1 和 结论2 大大减少了求循环节的时间。但是在本题还是不够,还需要优化。还能优化的地方就只有求约数的 O ( m ) O(\sqrt m) O(m)复杂度了,我们可以通过质因数分解在 O ( l o g 2 m ) O(log_2^m) O(log2m)时间内分解约数,于是最终复杂度就是 O ( q l o g 2 n ) O(q\:log_2^n) O(qlog2n),可以通过了。
代码示例: 见附录部分code-2:Horrible Poem
附录
code-1:Power Strings
#include<cstdio>
#include<iostream>
#include<cstring>
using namespace std;
const int N = 1e6+10;
char str[N];
int nex[N];
void getNex(char* str){
memset(nex,0,sizeof nex);
for(int i = 2,j = 0;str[i];i++){
while(j > 0 && str[i] != str[j+1]) j = nex[j];
if(str[i] == str[j+1]) j++;
nex[i] = j;
}
}
void solve(){
/*计算答案并输出*/
getNex(str);
int n = strlen(str)-1;
if(n%(n-nex[n])) puts("1");
else printf("%d\n",n/(n-nex[n]));
}
int main(){
while(scanf("%s",str+1) && str[1] != '.'){
str[0] = '*';
solve();
}
return 0;
}
code-2:Horrible Poem
#include<cstring>
#include<algorithm>
#include<cstdio>
using namespace std;
const int N = 5e5+10;
const int Q = 2e6+10;
char str[N];
int n,q;
typedef unsigned long long ull;
ull hsh[N],bse[N] , b = 31; //采用无符号长整形,通过自然溢出省去取模
bool check(int l,int r,int x){
/*判断x是否为子串s[l,r]的最短循环节长度*/
ull h1 = hsh[r-x] - hsh[l-1]*bse[r-x+1-l];
ull h2 = hsh[r] - hsh[l-1+x]*bse[r-x+1-l];
return h1 == h2;
}
int v[N],primes[N];
void getPri(){ //线性筛
int cnt = 0;
for(int i = 2;i <= n;i++){
if(!v[i]){
primes[cnt++] = i;
v[i] = i;
}
for(int j = 0;j < cnt;j++){
if(primes[j] > v[i] || primes[j]*i > N)
break;
v[i*primes[j]] = primes[j];
}
}
}
void ask(int l,int r){
/*回答子串s[l,r]的最短循环节长度*/
int len = r-l+1, ans = len, d = len;
while(d != 1){
int tmp = v[d];
while(d%tmp == 0 && check(l,r,ans/tmp)) d /= tmp,ans /= tmp;
while(d%tmp == 0) d /= tmp;
}
printf("%d\n",ans);
}
void solve(){
/*预处理出hash数组,v数组*/
getPri(); bse[0] = 1;
for(int i = 1;i <= n;i++){
hsh[i] = hsh[i-1]*b + str[i]-'a';
bse[i] = bse[i-1]*b;
}
}
inline int read() {
int x=0,f=1; char c=getchar();
while(c<'0'||c>'9') { if(c=='-') f=-1; c=getchar(); }
while(c>='0'&&c<='9') { x=x*10+c-'0'; c=getchar(); }
return x*f;
}
int main(){
n = read();
scanf("%s",str+1); str[0] = '#';
q = read();
solve();
for(int i = 1,l,r;i <= q;i++){
l = read(); r = read();
ask(l,r);
}
return 0;
}