1. 程式人生 > >KMP演算法的經典例題(poj 3461、poj 2752、poj 2406、poj1961)

KMP演算法的經典例題(poj 3461、poj 2752、poj 2406、poj1961)

最簡單的KMP題,找出第一個字串在第二個字串中出現次數。

#include <iostream>
#include <cstdio>
#include <cstring>
#define Memset(x, a) memset(x, a, sizeof(x))
using  namespace std;
const int N=1e6+10;
char  w[N],t[N];
int next[N];
int sum;

void  getNext(const char P[],int next[]){
  int  m=strlen(P);
  int i=0,j;
  j=next[0]=-1;
  while(i<m){
    while(-1!=j && P[i]!=P[j])j=next[j];
    next[++i]=++j;
  }
}

void  kmp(const char T[],const char P[],int next[]){
  int n=strlen(T),m=strlen(P);
  int i,j;
  getNext(P,next);
  i=j=0;
  while(i<n){
    while(-1!=j && T[i]!=P[j])j=next[j];
    i++;j++;
    if(j>=m){
      sum++;
      j=next[j];//這兒修改很重要,不然會超時
    }
  }
}

int main(){
  int T;
  scanf("%d",&T);
  while(T--){
    sum=0;
    Memset(next,0);
    scanf("%s%s",w,t);
    kmp(t,w,next);
    printf("%d\n",sum);
  }
  return 0;
}

思路:其實是next陣列的使用

下面給出描述: (i>1)[下標從0開始] 
next[i]的意義就是:前面長度為i的字串的【字首和字尾的最大匹配長度】 

那麼這題怎麼利用這個性質呢? 

詳細分析一下:【就用上面的第一個例子說明吧】 

求出next值:[非修正]
下標:      0  1  2  3  4  5  6  7  8  9  10 11 12 13 14 15 16 17
串:          a  b  a  b  c  a  b  a  b  a  b   a   b   c   a   b   a   b
next值:  -1  0  0  1  2  0  1  2  3  4  3   4   3   4   5   6   7   8   9

len2 = 18    next[len2] = 9 
說明對於前面長度為18的字串,【長度為9的字首】和【長度為9的字尾】是匹配的, 即上圖的藍色跟紅色匹配 
也就是整個串的最大前後綴匹配長度就是9了 
所以接下來根本不需要考慮長度大於9的情況啦 

好了!既然現在只需考慮長度小於9的前後綴匹配情況,那麼 
[問題就轉化成藍色串的字首紅色串的字尾的匹配問題了!!! 
又因為藍串==紅串
所以問題又轉化成 
找藍串自己的字首跟自己的字尾的最大匹配了!!!
那麼我們現在就要找next[9]的值了【next[9]的含義:藍串的最大前後綴匹配】 

回憶第一步:我們找的是next[len2]=9【len2=18】 
怎麼使得第二部目標變成9【求next[9]】呢? 

其實next[len2]=9同時可以表示為:最大匹配前後綴的【字首長度】 
那麼next[9]的意義就是: 
【主串】的最大匹配前後綴的【字首】的【最大匹配前後綴】了!! 
也就是上面藍串的前後綴最大匹配長度了!! 

那麼演算法描述就是: 
第一步:求next[len2], 即next[18] = 9; 
第二步:把9代進來,即求next[9] = 4; 
第三步:把4代進來,即求next[4] = 2; 
第四步:next[2] = 0; 也就是下標2之前的串已經沒有前後綴可以匹配了 
所以答案就是: 2 4 9 18 【PS: 從小到大輸出,18是串長,顯然符合題意】 
#include <iostream>
#include <cstdio>
#include <cstring>
#define Memset(x, a) memset(x, a, sizeof(x))
using  namespace std;
const int N=4e5+10;
int  next[N],ans[N];
char  s[N];

void  getNext(const char P[],int next[]){
  int  m=strlen(P);
  int i=0,j;
  j=next[0]=-1;
  while(i<m){
    while(-1!=j && P[i]!=P[j])j=next[j];
    next[++i]=++j;
  }
}

int main(){
  while(~scanf("%s",s)){
    Memset(next,0);
    getNext(s,next);
    int cnt=0;
    int len=strlen(s);
    int j=next[len];
    while(j>0){
      ans[++cnt]=j;
      j=next[j];
    }
    for(int i=cnt; i>0; i--)printf("%d ",ans[i]);
    printf("%d\n",len);
  }
  return 0;
}

思路:KMP,next表示模式串如果第i位(設str[0]為第0位)與文字串第j位不匹配則要回到第next[i]位繼續與文字串第j位匹配。則模式串第1位到next[n]與模式串第n-next[n]位到n位是匹配的。所以思路和上面一樣,如果n%(n-next[n])==0,則存在重複連續子串,長度為n-next[n]。

例如:a    b    a    b    a    b

next:-1   0    0    1    2    3    4

next[n]==4,代表著,字首abab與字尾abab相等的最長長度,這說明,ab這兩個字母為一個迴圈節,長度=n-next[n];

#include <iostream>
#include <cstdio>
#include <cstring>
#define Memset(x, a) memset(x, a, sizeof(x))
using  namespace std;
const int N=1e6+10;
int next[N];
char  s[N];

void  getNext(const char P[],int next[]){
  int  m=strlen(P);
  int i=0,j;
  j=next[0]=-1;
  while(i<m){
    while(-1!=j && P[i]!=P[j])j=next[j];
    next[++i]=++j;
  }
}

int main(){
  while(~scanf("%s",s)){
    if(s[0]=='.')break;
    Memset(next,0);
    getNext(s,next);
    int  len=strlen(s);
    if(len%(len-next[len])==0)printf("%d\n",len/(len-next[len]));
    else printf("1\n");
  }
  return 0;
}

題意:

給你一個字串,求這個字串到第i個字元為止的迴圈節的次數。

比如aabaabaabaab,長度為12.到第二個a時,a出現2次,輸出2.到第二個b時,aab出現了2次,輸出2.到第三個b時,aab出現3次,輸出3.到第四個b時,aab出現4次,輸出4.

思路:

這道題就是上題的加強版而已。上一道題輸出一個字串的迴圈節出現的次數,這個是到第i個字元為止,其實就是多了一層迴圈。把這個字串遍歷一次即可,具體思路也以參考小白書的例題。

#include <iostream>
#include <cstdio>
#include <cstring>
#define Memset(x, a) memset(x, a, sizeof(x))
using  namespace std;
const int N=1e6+10;
char s[N];
int next[N];
int n;

void  getNext(const char P[],int next[]){
  int  m=strlen(P);
  int i=0,j;
  j=next[0]=-1;
  while(i<m){
    while(-1!=j && P[i]!=P[j])j=next[j];
    next[++i]=++j;
  }
}

int main(){
  int kase=0;
  while(~scanf("%d",&n)&&n){
    scanf("%s",s);
    Memset(next,0);
    getNext(s,next);
    printf("Test case #%d\n",++kase);
    for(int i=2; i<=n; i++){
      if(next[i]>0&&i%(i-next[i])==0)printf("%d %d\n",i,i/(i-next[i]));
    }
    printf("\n");
  }
  return 0;
}