關於最小循環節的幾種求法
鄒毅
對於任何信息,人類總有一種沖動,就是找到其最本質的組成。例如對於所有的數字,我們會去研究質數,那是因為質數可不可再分解的,於是任何整數都可以寫成質因子連乘的形式。對於字符串,看似無規律,但由於語法上的原因,事實上許多字符串其用到的字符種類是不太多的,也就是說字母表中的26個字母出現的頻率是不一樣的。於是人類開始研究最小循環節,即某個字符串是不是由某個循環節字符串拼接而成。我們來看下面這個例題:
Pku2406 Power Strings
求一個字符串由多少個重復的子串連接組成,例如ababab由3個ab連接而成,因此答案為3,又例如abcd由1個abcd連接而成,因此答案為1
Format
Input
多組數據,以"."代表測試結束 每組數據給出的字符串長度 <=1e6
Output
如題
樣例輸入
abcd
aaaa
ababab
.
樣例輸出
1
4
3
題解1:
對於這個題,我們設讀入的字符串存在字符數組s中,設其長度為len.
於是可以枚舉所求的循環節長度為i,即字符數組的前i個字符構成了循環節,然后就可以來進行校驗了。由於此處涉及字符的比較,於是使用hash。
#include<iostream>
#include<cstdio>
#include<string>
#include<cstring>
using namespace std;
typedef unsigned long long LL;
const LL base=131;
const int N=1000010;
int n;
LL power[N],sum[N];
bool check(LL v,int k) //判斷s[1]~s[k]是否是循環節
{
for(register int i=1;i+k-1<=n;i+=k){
if(v!=sum[i+k-1]-sum[i-1]*power[k]) return 0;
}
return 1;
}
int main()
{
power[0]=1;
for(register int i=1;i<=N-10;++i) //hash准備工作
power[i]=power[i-1]*base;
char s[N];
while(scanf("%s",s+1)){
if(s[1]=='.')break;
n=strlen(s+1);
sum[0]=0;
for(register int i=1;i<=n;++i) sum[i]=sum[i-1]*base+LL(s[i]);
for(register int i=1;i<=n;++i){
if(n%i)continue;
LL expect=sum[i];
if(check(expect,i)){
printf("%d\n",n/i);
break;
}
}
}
return 0;
}
題解2:
在上種做法中,我們設循環節長度為i ,當然i必然為len的約數。於是整個字符串分成了len/i份。然后逐個逐個比較過去。大膽猜想一下,能否不要比較這么多次呢?
我們來畫個圖看看,對於字符串s划分如下:
![]()
為了區分,這幾段標上了不同的顏色。
如果第一段為我們所求的循環節,則我們將s復寫一次,並右移i 位

如果a2—a5這一段等於下面的a1—a4這一段,則可知
A2=a1,a3=a2,a4=a3,a5=a4.
於是循環節為A1.
分析出這個性質后,我們只需要一次字符之間的對比,就可以知道字符串的某個前綴是不是整個字符串的循環節了。
#include<bits/stdc++.h>
using namespace std;
typedef unsigned long long ull;
char a[2000000];
int len=0;
ull sum[2000000],power[2000000];
ull get(int l,int r)
{
return sum[r]-sum[l-1]*power[r-l+1];
}
int main()
{
while(true)
{
scanf("%s",a+1);
len=strlen(a+1);
if(a[1]=='.'&&len==1)break;
memset(sum,0,sizeof(sum));
memset(power,0,sizeof(power));
for(int i=1;i<=len;i++)
sum[i]=sum[i-1]*193+ull(a[i])+1;
power[0]=1;
for(int i=1;i<=len;i++)
power[i]=power[i-1]*193;
for(int i=1;i<=len;i++) //暴力枚舉循環節的長度
{
if(len%i!=0)continue;
else
{
ull a1=get(1,len-i),a2=get(i+1,len);
//注意是取長度為len-i的前綴,看是否等於長度為len-i的后綴
if(a1==a2)
{
printf("%d\n",len/i);//得到循環節的個數
break;
}
}
}
}
return 0;
}
Sol3:
題解2中,減少了比較的次數,看上去似乎沒有優化的地步了。我們將眼光轉向循環節的長度這個要素。在前面的做法中,我們都只要求循環節長度i為總長度len的約數即可,於是划分的段數 k=len/i,完全沒有考慮讀入字符串的構成這個因素。很明顯我們可以統計下字符串中每種字母出現的次數,不妨設之為sum1……sum26,當我們根據循環節將整個字符串划分成k段時,就是將這些字母“均分”到k段中,於是k至多為gcd(len,sum1,sum2….sum26),如果檢測不成功,則也應該為 gcd(len,sum1,sum2….sum26)的約數,至此我們較為精確的約束了k范圍.
#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long ll;
const ll M=1000010;
const ll b=193;
char c[M];
ll hh[M],sum[M],len,ans,num[30],gcd;
ll cut(ll l,ll r) {
return sum[r]-sum[l-1]*hh[r-l+1];
}
bool check(ll k) {
return cut(1,len-k)==cut(k+1,len);
}
main() {
hh[0]=1;
for(ll i=1; i<=M; i++)
hh[i]=hh[i-1]*b;
while(scanf("%s",c+1))
{
if(c[1]=='.')break;
memset(num,0,sizeof num);
sum[0]=0;
len=strlen(c+1);
for(ll i=1; i<=len; i++)
sum[i]=sum[i-1]*b+ll(c[i]);
for(int i=1; i<=len; i++)
num[c[i]-'a'+1]++;
// for(int i=1;i<=26;i++)
// cout<<num[i]<<" ";
gcd=len;
for(int i=1; i<=26; i++)
if (num[i])
gcd=__gcd(gcd,num[i]);
//cout<<"gcd is "<<gcd<<endl;
for(ll i=gcd; i>=1; i--)
{
if(gcd%i!=0)
continue;
ll num=sum[len/i];
if(check(len/i))
{
printf("%lld\n",i);
break;
}
}
}
}
題后記:此題還可以用Kmp算法來完成,此其就不再細說了,有興趣的可以自行去研究。
