字符串最短循环节

循环节与最短循环节:
若某个字符串是由某个子串循环构成的,那么就称该子串为原串的循环节,长度最短的循环节就是最短循环节。
如abababab,abab和ab都是原串的循环节,而最短循环节是ab。

结论:
如果字符串 s 有个循环节 son,n = |s| , x = |son|,字符数组下标从1开始,那么:

  1. x 一定是 n 的约数。
  2. 那么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] 。

证明:反证法

  1. 首先根据结论2,因为 s[1,nex[n] ] = s[n - nex[n] , n],所以必然存在长度为 x = n - nex[n]的循环节,问题就在于它是不是最短的。
  2. 假设存在循环节,长度为 y(y < x) ,那么根据结论2,一定有: s[1, n-y] = s[y , n];如此一来nex[n] = n-y > n-x,这和nex数组定义矛盾,因此不存在y < x。
  3. 综上所述,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;
}
  • 4
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

迷亭1213

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值