KMP 求最小循環節


轉載自:https://www.cnblogs.com/chenxiwenruo/p/3546457.html

KMP模板,最小循環節

 

 

下面是有關學習KMP的參考網站

http://blog.csdn.net/yaochunnian/article/details/7059486

http://blog.csdn.net/v_JULY_v/article/details/6111565

http://blog.csdn.net/v_JULY_v/article/details/6545192

http://blog.csdn.net/oneil_sally/article/details/3440784

http://billhoo.blog.51cto.com/2337751/411486

 

先說說next數組的含義:

next[i]就是前面長度為i的字符串前綴和后綴相等的最大長度,也即索引為i的字符失配時的前綴函數。

下面幾個版本的next函數,除了next[0]不同外(版本一中為-1,版本二中為0),其余無差別

 

一:KMP算法模板

版本一:

//求str對應的next數組
void getNext(char const* str, int len)
{
    int i = 0;
    next[i] = -1;
    int j = -1;
    while( i < len )
    {
        if( j == -1 || str[i] == str[j] )   //循環的if部分
        {
            ++i;
            ++j;
            //修正的地方就發生下面這4行
            if( str[i] != str[j] ) //++i,++j之后,再次判斷ptrn[i]與ptrn[j]的關系
                next[i] = j;      //之前的錯誤解法就在於整個判斷只有這一句。
            else
                next[i] = next[j];  //這里其實是優化了后的,也可以仍是next[i]=j
            //當str[i]==str[j]時,如果str[i]匹配失敗,那么換成str[j]肯定也匹配失敗,
            //所以不是令next[i]=j,而是next[i] = next[j],跳過了第j個字符,
            //即省去了不必要的比較
            //非優化前的next[i]表示前i個字符中前綴與后綴相同的最大長度
        }
        else                                 //循環的else部分
            j = next[j];
    }
}

//在目標字符串target中,字符str出現的個數
//n為target字符串的長度,m為str字符串的長度
int kmp_match(char *target,int n,char *str,int m){
    int i=0,j=0;  //i為target中字符的下標,j為str中字符的下標
    int cnt=0;   //統計str字符串在target字符串中出現的次數
    while(i<=n-1){
        if(j<0||target[i]==str[j]){
            i++;
            j++;
        }
        else{
            j=next[j]; //當j=0的時候,suffix[0]=-1,這樣j就會小於0,所以一開始有判斷j是否小於0
        }

        //str在target中找到匹配
        if(j==m){
            cnt++;
            j=next[j];
        }
    }
    return cnt;
}
//在目標字符串target中,若存在str字符串,返回匹配成功的第一個字符的位置
int kmp_search(char *target,int n,char *str,int m){
    int i=0,j=0;  //i為target中字符的下標,j為str中字符的下標
    int cnt=0;   //統計str字符串在target字符串中出現的次數
    while(i<n && j<m){
        if(j<0||target[i]==str[j]){
            i++;
            j++;
        }
        else{
            j=suffix[j]; //當j=0的時候,suffix[0]=-1,這樣j就會小於0,所以一開始有判斷j是否小於0
        }
    }
    if(j>=m)
        return i-m;
    else
        return -1;
}
View Code

版本二(算法導論):

//這里的next和前面一樣,next[i]就是前面長度為i的字符串前綴和后綴相等的長度,
//即索引為i的字符失配時的前綴函數
void getNext(char *str,int m){
    memset(next,0,sizeof(next));
    next[1]=0;
    int k=0;
    for(int i=2;i<=m;i++){
        while(k>0 && str[k]!=str[i-1])
            k=next[k];
        if(str[k]==str[i-1])
            k++;
        next[i]=k;
    }
}
//n為target字符串的長度,m為str字符串的長度,統計str在target中出現的個數
int match(char *target,int n,char * str,int m){
    int k=0,cnt=0;
    for(int i=0;i<n;i++){
        while(k>0 && str[k]!=target[i])
            k=next[k];
        if(str[k]==target[i])
            k++;
        if(k==m){
            cnt++;
            k=next[k];
        }
    }
    return cnt;
}

//n為target字符串的長度,m為str字符串的長度
//若存在str字符串,返回匹配成功的第一個字符的位置
int match(char *target,int n,char * str,int m){
    int k=0,cnt=0;
    for(int i=0;i<n;i++){
        while(k>0 && str[k]!=target[i])
            k=next[k];
        if(str[k]==target[i])
            k++;
        if(k==m){
            return i-m+1;
        }
    }
    return -1;
}
View Code

某大神的模板(其實和算法導論一樣):

#define KMP_GO(X) while(k>0 && P[k]!=X[i]) k=next[k];if(P[k]==X[i])k++
//求字符串P在T中出現的次數
int kmp_match(char*T,char*P){
    int n,m,next[10010],i,k,c;
    n=strlen(T);m=strlen(P);
    next[1]=k=0;
    for(i=1;i<m;i++){
        KMP_GO(P);
        next[i+1]=k;//這里i表示的是字符的索引,對應的長度i+1
    }
    k=c=0;
    for(i=0;i<n;i++){
        KMP_GO(T);
        if(k==m){
            c++;
            k=next[k];
        }
    }
    return c;
}
View Code

 

二:KMP最小循環節、循環周期:

定理:假設S的長度為len,則S存在最小循環節,循環節的長度L為len-next[len],子串為S[0…len-next[len]-1]。

(1)如果len可以被len - next[len]整除,則表明字符串S可以完全由循環節循環組成,循環周期T=len/L。

(2)如果不能,說明還需要再添加幾個字母才能補全。需要補的個數是循環個數L-len%L=L-(len-L)%L=L-next[len]%L,L=len-next[len]。

 

 

理解該定理,首先要理解next數組的含義:next[i]表示前面長度為i的子串中,前綴和后綴相等的最大長度。

如:abcdabc

index

0

1

2

3

4

5

6

7

char

a

b

c

d

a

b

C

 

next

-1

0

0

0

0

1

2

3

 

如對於a,ab,abc,abcd,很明顯,前綴和后綴相同的長度為0

  對於長度為5的子串abcda,前綴的a和后綴的a相同,長度為1

  對於長度為6的子串abcdab,前綴的ab和后綴的ab相同,長度為2

接下來舉幾個例子來說明最小循環節和循環周期:

為方便說明,先設字符串的長度為len,循環子串的長度為L

1.

s0s1s2s3s4s5 ,next[6]=3

即s0s1s2=s3s4s5

很明顯可知:循環子串為s0s1s2,L=len-next[6]=3,且能被len整除。

 

2.

s0s1s2s3s4s5s6s7 ,next[8]=6

此時len-next[8]=2 ,即L=2

由s0s1s2s3s4s5=s2s3s4s5s6s7

可知s0s1=s2s3,s2s3=s4s5,s4s5=s6s7

顯然s0s1為循環子串

 

3.

s0s1s2s3s4s5s6 ,next[7]=4

此時len-next[7]=3,即L=3

由s0s1s2s3=s3s4s5s6

可知s0s1=s3s4,s2s3=s5s6

從而可知s0s1s2=s3s4s5,s0=s3=s6

即如果再添加3-4%3=2個字母(s1s2),那么得到的字符串就可以由s0s1s2循環3次組成

 

這個定理可以這么理解:

http://www.cnblogs.com/oyking/p/3536817.html

對於一個字符串,如abcd abcd abcd,由長度為4的字符串abcd重復3次得到,那么必然有原字符串的前八位等於后八位。

也就是說,對於某個字符串S,長度為len,由長度為L的字符串s重復R次得到,當R≥2時必然有S[0..len-L-1]=S[L..len-1],字符串下標從0開始

那么對於KMP算法來說,就有next[len]=len-L。此時L肯定已經是最小的了(因為next的值是前綴和后綴相等的最大長度,即len-L是最大的,那么在len已經確定的情況下,L是最小的)。

 

如果一定仔細證明的話,請看下面:

(參考來自:http://www.cnblogs.com/wuyiqi/archive/2012/01/06/2314078.html,有所改動)

 k    m   x     j     i

由上,next【i】=j,兩段紅色的字符串相等(兩個字符串完全相等),s[k....j]==s[m....i]

設s[x...j]=s[j....i](xj=ji)

則可得,以下簡寫字符串表達方式

kj=kx+xj;

mi=mj+ji;

因為xj=ji,所以kx=mj,如下圖所示

 k   m     a    x    j     i

設s[a…x]=s[x..j](ax=xj)

又由xj=ji,可知ax=xj=ji

即s[a…i]是由s[a…x]循環3次得來的。

而且看到沒,此時又重復上述的模型,s[k…x]=s[m…j],可以一直遞推下去

最后可以就可以遞推出文章開頭所說的定理了。

 

最后再舉兩個相關例子

abdabdab  len:8 next[8]:5

最小循環節長度:3(即abd)   需要補的個數是1  d

ababa  len:5 next[5]:3

最小循環節長度:2(即ab)    需要補的個數是1  b

 

一道求循環節的題目

題目鏈接:https://oj.ismdeep.com/contest/Problem?id=1284&pid=6

G: Dave的時空迷陣

Time Limit: 1 s      Memory Limit: 128 MB     

Problem Description

皇家理工本部隱藏着一座扭曲時空的迷陣,一旦陷入迷陣就不能復出。Dave作為一個勇敢的探險家,勇敢闖入迷陣,並發現了一些規律……

Dave發現總是在行進一定距離后回到起點,繼續走上重復的路途….

冷靜分析之后,Dave在前進的路途中記錄了標記(a-z的小寫字母),並得到了一個字符串,Dave想知道,從起點開始,最少走多遠會回到初始狀態?

Input

第一行一個正整數nn為Dave記錄的字符串長度(1n2×105)(1≤n≤2×105)

第二行為長度nn的字符串,僅包含aza−z的小寫英文字母的非空字符串

Output

第一行輸出從起點再到起點的距離

第二行輸出行進路上遇到的字符

Sample Input

4
abcd

Sample Output

4
abcd

解題思路:對於給定串求出 next 數組,利用循環節性質得出是否滿足循環,再將末尾多余的字母隔過 去,輸出一個整的循環節即可


附上代碼
#include <bits/stdc++.h>
using namespace std;
const int AX = 3e5+666;  
char ps[AX];
int len ;
int next1[AX];
void getnext1( int m )
{
    next1[0] = -1;
    int j = 0;
    int k = -1;
    while ( j < m ) 
    {
        if (k == -1 || ps[j] == ps[k])
            next1[++j] = ++k;
        else
            k = next1[k];
    }
}
int main()
{
    scanf("%d",&len);
    scanf("%s",ps);
    getnext1(len);

    int ans = len - next1[len];
    int left = next1[len] % ans ;
    printf("%d\n",ans);
    for( int i = len - left - ans ; i < len - left ; i ++ )
        printf("%c",ps[i]);
    printf("\n");
    return 0;
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM