1. 程式人生 > >poj 2406 poj 1961 個人對吉大KMP模板的理解 KMP 基礎題--找週期串

poj 2406 poj 1961 個人對吉大KMP模板的理解 KMP 基礎題--找週期串

好佩服寫kmp程式碼的人...   看死了終於看得有點明白了......大哭

學kmp先看兩個比較好的總結

這兩個講解非常好

摘一句我認為最重要的話

預處理出這樣一個數組P[j],表示當匹配到B陣列的第j個字母而j+1個字母不能匹配了時,新的j最大是多少P[j]應該是所有滿足B[1..P[j]]=B[j-P[j]+1..j]的最大值。

(B陣列就是模式串)

額,似乎難以理解,那我就摘抄下matrix67的話吧:

假如,A="abababaababacb",B="ababacb",我們來看看KMP 是怎麼工作的。我們用兩個指標i和j分別表示,A[i-j+ 1..i]與B[1..j]完全相等。也就是說,i是不斷增加的,隨著i的增加j相應地變化,且j滿足以A[i]結尾的長度為j的字串正好匹配B串的前 j個字元(j當然越大越好),現在需要檢驗A[i+1]和B[j+1]的關係。當A[i+1]=B[j+1]時,i和j各加一;什麼時候j=m了,我們就 說B是A的子串(B串已經整完了),並且可以根據這時的i值算出匹配的位置。當A[i+1]<>B[j+1],KMP的策略是調整j的位置 (減小j值)使得A[i-j+1..i]與B[1..j]保持匹配且新的B[j+1]恰好與A[i+1]匹配(從而使得i和j能繼續增加)。我們看一看當 i=j=5時的情況。

    i = 1 2 3 4 5 6 7 8 9 ……

    A = a b a b a b a a b a b …

    B = a b a b a c b

    j = 1 2 3 4 56 7

    此時,A[6]<>B[6]。這表明,此時j不能等於5了,我們要把j改成比它小的值j'。j'可能是多少呢?仔細想一下,我們發現,j'必須 要使得B[1..j]中的頭j'個字母和末j'個字母完全相等(這樣j變成了j'後才能繼續保持i和j的性質)。這個j'當然要越大越好。在這裡,B[1..5]="ababa",頭3個字母和末3個字母都是"aba"。而當新的j為3時,A[6]恰好和B[4]相等。於是,i變成了6,而j則變成了 4:

    i = 1 2 3 4 5 6 7 8 9 ……

    A = a b a b a b a a b a b …

    B =     a b a b a cb

    j =     1 2 3 4 5 67

    從上面的這個例子,我們可以看到,新的j可以取多少與i無關,只與B串有關。我們完全可以預處理出這樣一個數組P[j],表示當匹配到B陣列的第j個字母而j+1個字母不能匹配了時,新的j最大是多少P[j]應該是所有滿足B[1..P[j]]=B[j-P[j]+1..j]的最大值。

    再後來,A[7]=B[5],i和j又各增加1。這時,又出現了A[i+1]<>B[j+1]的情況:

    i = 1 2 3 4 56 7 8 9 ……

    A = a b a b ab a a b a b …

    B =     a b a b a cb

    j =     1 2 3 4 5 67

    由於P[5]=3,因此新的j=3:

    i = 1 2 3 4 56 7 8 9 ……

    A = a b a b ab a a b a b …

    B =         a b a ba c b

    j =         1 2 3 45 6 7

    這時,新的j=3仍然不能滿足A[i+1]=B[j+1],此時我們再次減小j值,將j再次更新為P[3]:

    i = 1 2 3 4 56 7 8 9 ……

    A = a b a b ab a a b a b …

    B =             a ba b a c b

    j =             1 23 4 5 6 7

    現在,i還是7,j已經變成1了。而此時A[8]居然仍然不等於B[j+1]。這樣,j必須減小到P[1],即0:

    i = 1 2 3 4 56 7 8 9 ……

    A = a b a b ab a a b a b …

    B =               a b a b a c b

    j =             0 12 3 4 5 6 7

    終於,A[8]=B[1],i變為8,j為1。事實上,有可能j到了0仍然不能滿足A[i+1]=B[j+1](比如A[8]="d"時)。因此,準確的說法是,當j=0了時,我們增加i值但忽略j直到出現A[i]=B[1]為止。

我用的是吉大的程式碼(似乎寫法跟主流不太一樣?):

int fail[P];

int kmp(char* str, char* pat)
{
    int i, j, k;
    memset(fail, -1, sizeof(fail));
    for(i = 1; pat[i]; ++i)
    {
        for(k=fail[i-1]; k>=0 && pat[i]!=pat[k+1];k=fail[k]);
        if(pat[k + 1] == pat[i]) fail[i] = k + 1;
    }
    i = j = 0;
    while( str[i] && pat[j] )
    {// By Fandywang
        if( pat[j] == str[i] ) ++i, ++j;
        else if(j == 0)++i;//第一個字元匹配失敗,從str下個字元開始
        else j = fail[j-1]+1;
    }
    if( pat[j] )return -1;
    else return i-j;
}

說了這麼多,全是抄的別人的...現在該談我自己的理解了:
KMP程式碼做了兩件事:一是處理模式串(有用pat,pattern,B表示)整理出fail陣列(也有說next陣列,matrix67說的是P);

二是從原串中查詢模式串;

重複一下那句最重要的話:預處理出這樣一個數組P[j],表示當匹配到B陣列的第j個字母而j+1個字母不能匹配了時,新的j最大是多少P[j]應該是所有滿足B[1..P[j]]=B[j-P[j]+1..j]的最大值。 

如fail[i]=k;則從pat[0]到pat[i]一共i+1個字元,fail[i]=k意味著從fail[0]到fail[i-1]這i個字元0到k-1這前k個字元形成的子串與i-k到i-1這k個字元形成的子串相同,如abcdabcd,fail[5]=1,就是“abcda”,pat[0]==pat[4],所以是1,

這個例子跟poj 2406還不是很接近,那麼再看一個例子:
abcabcabc   它的fail[8]=5,前5個字元為"abcab",後5個字元為“abcab”,但因為下標從0開始,並且原串就是由一個子串重複多次得到的,所以可以斷言,pat[8]==pat[5],就是說前6個字元和後六個字元相同,再看看,原串長度9-6剛好等於一個週期    pe=len-fail[len-1]-1;那麼週期數就是len/pe;

貼程式碼吧:

//141MS
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define N 1000003

int fail[N];
char str[N];

void KMP()
{
    int len=strlen(str);
    int i=0,k;

    memset(fail,-1,sizeof(fail));
    /*
    k=-1;
    while(i<len)
    {
        if(k==-1||str[i]==str[k])
            fail[++i]=++k;
        else
            k=fail[k];
    }
    i=len-k;//如果最後一個位置不匹配,那麼就會滾到len-k的位置,也就是最小重複字串的長度。
    if(len%i==0)
        return len/i;
    else
        return 1;
    */
    for(i=1;str[i];i++)
    {
        for(k=fail[i-1];k>=0&&str[k+1]!=str[i];k=fail[k]);
        if(str[k+1]==str[i])fail[i]=k+1;
    }
    int pe=len-fail[len-1]-1;
    if(len%pe==0)printf("%d\n",len/pe);
    else printf("1\n");
}

int main()
{
    while(scanf("%s",str)!=EOF)
    {
        if(!strcmp(str,"."))break;
        KMP();
    }
    return 0;
}

註釋掉的是另一種寫法...懶得去學了....

知道這個題再去看poj 1961 就很容易做了;

題意是,字首如果是週期串,找出它由幾個週期組成,典型的poj 2406翻版,不過是檢測原串的從0到i形成的字串是不是週期串(i<=len-1)

貼程式碼:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define N 1000003

int fail[N],len;
char str[N];

void kmp()
{
    int i,k;

    memset(fail,-1,sizeof(fail));
    for(i=1;i<len+1;i++)
    {
        for(k=fail[i-1];k>=0&&str[k+1]!=str[i];k=fail[k]);
        if(str[k+1]==str[i])fail[i]=k+1;
        int t=fail[i];
       if((i+1)%(i-t)==0&&(i+1)/(i-t)>1)printf("%d %d\n",i+1,(i+1)/(i-t));
    }
}

int main()
{
    //freopen("in.txt","r",stdin);
    int ncase=1;

    while(scanf("%d",&len),len)
    {
        printf("Test case #%d\n",ncase++);
        scanf("%s",str);
        kmp();
        putchar('\n');
    }

    return 0;
}

如果還是不明白kmp,建議模擬,多列印fail陣列,k的值,看規律,依據那句最重要的話理解就好