關於最小循環節的幾種求法
鄒毅
對於任何信息,人類總有一種沖動,就是找到其最本質的組成。例如對於所有的數字,我們會去研究質數,那是因為質數可不可再分解的,於是任何整數都可以寫成質因子連乘的形式。對於字符串,看似無規律,但由於語法上的原因,事實上許多字符串其用到的字符種類是不太多的,也就是說字母表中的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算法來完成,此其就不再細說了,有興趣的可以自行去研究。