關於最小迴圈節的幾種求法[原創]

oiteacher發表於2021-11-29

 

對於任何資訊,人類總有一種衝動,就是找到其最本質的組成。例如對於所有的數字,我們會去研究質數,那是因為質數可不可再分解的,於是任何整數都可以寫成質因子連乘的形式。對於字串,看似無規律,但由於語法上的原因,事實上許多字串其用到的字元種類是不太多的,也就是說字母表中的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範圍,程式碼略過。

 

相關文章