轉載自:https://www.cnblogs.com/chenxiwenruo/p/3546457.html
下面是有關學習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;
}
版本二(算法導論):


//這里的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;
}
某大神的模板(其實和算法導論一樣):


#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;
}
二: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的時空迷陣
Problem Description
皇家理工本部隱藏着一座扭曲時空的迷陣,一旦陷入迷陣就不能復出。Dave作為一個勇敢的探險家,勇敢闖入迷陣,並發現了一些規律……
Dave發現總是在行進一定距離后回到起點,繼續走上重復的路途….
冷靜分析之后,Dave在前進的路途中記錄了標記(a-z的小寫字母),並得到了一個字符串,Dave想知道,從起點開始,最少走多遠會回到初始狀態?
Input
第一行一個正整數nn為Dave記錄的字符串長度(1≤n≤2×105)(1≤n≤2×105)
第二行為長度nn的字符串,僅包含a−za−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; }