1. 程式人生 > >KMP模板以及簡單的入門題總結

KMP模板以及簡單的入門題總結

KMP模板

//kmp演算法的主要作用在於對next陣列的運用,所以這裡只給出next陣列的模板
//性質1:對於每一個長度len的子串,該子串的最小迴圈節為len-next[len]
//性質2:kmp的next不斷向前遞迴的過程可以保證對於每一個當前字首,都有一段字尾與之對應
#include<stdio.h>
#include<iostream>
#include<string.h>
using namespace std ;
const int maxn = 1e6+5;
int Next[maxn];
char mo[maxn];
int n2;
void
GetNext() { int i=0,j=-1; while(i<n2) { if(j==-1||mo[i]==mo[j]) {++i,++j,Next[i]=j;} else j=Next[j]; } return ; } int main() { scanf("%s",mo); n2=strlen(mo); Next[0]=-1; GetNext(); return 0; }

KMP第一題

HDU1711
題意就是給你兩個序列,讓你求B序列在A序列第一次出現(完全相同)的下標
本題就是KMP的模板題,將i指標指向A串,將j指標指向B串,如果匹配就繼續下一位的匹配,如果不匹配,將j跳轉到next[j],繼續向前匹配。
HDU1711程式碼

#include<stdio.h>
#include<iostream>
using namespace std ;
const int maxn = 1e6+5;
int Next[maxn];
int str[maxn];
int mo[maxn];
int n1,n2;
void GetNext()
{
    int i=0,j=-1;
    while(i<n2)
    {
        if(j==-1||mo[i]==mo[j]) {++i,++j,Next[i]=j;}
        else j=Next[j];
    }
    return
; } int kmp() { int cnt=0; int i=0,j=0; while(i<n1) { if(j==-1||str[i]==mo[j]) i++,j++; else j=Next[j];//next陣列尋找與當前字尾匹配最長的字首,省略不必要的查詢 if(j==n2) return i-n2+1;//首地址 } return -1; } int main() { int t; scanf("%d",&t); while(t--) { scanf("%d%d",&n1,&n2); for(int i=0;i<n1;i++) scanf("%d",&str[i]); for(int j=0;j<n2;j++) scanf("%d",&mo[j]); Next[0]=-1; GetNext(); printf("%d\n",kmp()); } return 0; }

KMP第二題

HDU1686
題意就是求B串在A串中的出現次數(可重疊
依舊是利用next陣列,當某次匹配完成之後,將ans++,然後把j跳轉到next[j],原理類似上一題的不匹配的情況,省略不必要的查詢。
HDU1686程式碼

#include<stdio.h>
#include<iostream>
#include<string.h>
using namespace std ;
const int maxn = 1e6+5;
int Next[maxn];
char str[maxn];
char mo[maxn];
int n1,n2;
void GetNext()
{
    int i=0,j=-1;
    while(i<n2)
    {
        if(j==-1||mo[i]==mo[j]) {++i,++j,Next[i]=j;}
        else j=Next[j];
    }
    return ;
}
int kmp()
{
    int cnt=0;
    int i=0,j=0;
    while(i<n1)
    {
        if(j==-1||str[i]==mo[j]) i++,j++;
        else j=Next[j];
        if(j==n2)
        {
            cnt++;
            j=Next[j];//完成一次匹配,將j移動到最長的字首處,省略不必要的查詢
        }
    }
    return cnt;
}
int main()
{
    int t;
    scanf("%d",&t);
    while(t--)
    {
        scanf("%s%s",mo,str);
        n1=strlen(str);
        n2=strlen(mo);
        Next[0]=-1;
        GetNext();
        printf("%d\n",kmp());
    }
    return 0;
}

KMP第三題

HDU2087
題意就是求B串在A串中的出現次數(不可重疊)
做法和上題類似,只不過在每次完成匹配之後,要考慮不能重疊的問題,所以j不能跳轉到next[j],而應該從0重新開始,因為A中已經匹配過的字元不能再利用,所以就變成了A剩下的子串中找B出現的次數,所以j應該從0重新開始匹配。
HDU2087程式碼

#include<stdio.h>
#include<iostream>
#include<string.h>
using namespace std ;
const int maxn = 1e6+5;
int Next[maxn];
char str[maxn];
char mo[maxn];
int n1,n2;
void GetNext()
{
    int i=0,j=-1;
    while(i<n2)
    {
        if(j==-1||mo[i]==mo[j]) {++i,++j,Next[i]=j;}
        else j=Next[j];
    }
    return ;
}
int kmp()
{
    int cnt=0;
    int i=0,j=0;
    while(i<n1)
    {
        if(j==-1||str[i]==mo[j]) i++,j++;
        else j=Next[j];
        if(j==n2)
        {
            cnt++;
            j=0;
        }
    }
    return cnt;
}
int main()
{
    while(scanf("%s",str)!=EOF)
    {
        n1=strlen(str);
        if(n1==1&&str[0]=='#') break;
        scanf("%s",mo);
        n2=strlen(mo);
        Next[0]=-1;
        GetNext();
        printf("%d\n",kmp());
    }
    return 0;
}

KMP第四題

HDU3746
本題題意為新增最少的字元使原字串變成周期至少為2的迴圈字串
用到模板裡所說的,長度為len的字串的最小迴圈節為len-next[len],求出最小迴圈節,算出最後應該補充多少就結束了。
求最小迴圈節的證明法請看此連結KMP求字串最小迴圈節
HDU3746程式碼

#include<stdio.h>
#include<iostream>
#include<string.h>
using namespace std ;
const int maxn = 1e6+5;
int Next[maxn];
char str[maxn];
char mo[maxn];
int n1,n2;
void GetNext()
{
    int i=0,j=-1;
    while(i<n2)
    {
        if(j==-1||mo[i]==mo[j]) {++i,++j,Next[i]=j;}
        else j=Next[j];
    }
    return ;
}
int main()
{
    int t;
    scanf("%d",&t);
    while(t--)
    {
        scanf("%s",mo);
        n2=strlen(mo);
        Next[0]=-1;
        GetNext();
        int tmp=n2-Next[n2];
        if(n2%tmp==0&&n2!=tmp)
        {
            printf("0\n");
        }
        else
        {
            printf("%d\n",tmp-n2%tmp);
        }
    }
    return 0;
}

KMP第五題

HDU1358
本題題意為求給定字串中所有為迴圈串的字首,並輸出該字首的最後一個字元下標和週期
用到模板中給定的方法,只要對每個字串判定一下是否為迴圈串就可以了
HDU1358程式碼

#include<stdio.h>
#include<iostream>
#include<string.h>
using namespace std ;
const int maxn = 1e6+5;
int Next[maxn];
char str[maxn];
char mo[maxn];
int n1,n2;
void GetNext()
{
    int i=0,j=-1;
    while(i<n2)
    {
        if(j==-1||mo[i]==mo[j]) {++i,++j,Next[i]=j;}
        else j=Next[j];
    }
    return ;
}
int main()
{
    int cnt=1;
    while(scanf("%d",&n2)!=EOF)
    {
        if(n2==0) break;
        scanf("%s",mo);
        Next[0]=-1;
        GetNext();
        printf("Test case #%d\n",cnt++);
        for(int i=1;i<=n2;i++)
        {
            int tmp=i-Next[i];
            if(i/tmp==1) continue;
            if(i%tmp==0)
            {
                printf("%d %d\n",i,i/tmp);
            }
        }
        printf("\n");
    }
    return 0;
}

KMP第六題

POJ2406
本題題意為求給定字串的最大週期,做法類似上題,直接上程式碼
POJ2406程式碼

#include<stdio.h>
#include<iostream>
#include<string.h>
using namespace std ;
const int maxn = 1e6+5;
int Next[maxn];
char str[maxn];
char mo[maxn];
int n1,n2;
void GetNext()
{
    int i=0,j=-1;
    while(i<n2)
    {
        if(j==-1||mo[i]==mo[j]) {++i,++j,Next[i]=j;}
        else j=Next[j];
    }
    return ;
}
int main()
{
    int cnt=1;
    while(scanf("%s",mo)!=EOF)
    {
        n2=strlen(mo);
        if(n2==1&&mo[0]=='.') break;
        Next[0]=-1;
        GetNext();
        int tmp=n2-Next[n2];
        if(n2%tmp==0) printf("%d\n",n2/tmp);
        else printf("1\n");
    }
    return 0;
}

KMP第七題

POJ2752
本題題意為求出所有在後綴中出現過的字首的最後一個元素的下標
本題要考慮一下next陣列的本質,其實就是最長的出現在後綴中的字首,但是由於本題要求所有的而不是最長的,考慮到next陣列的遞迴過程,其實就是對每一個當前長度的字首,都有完全相同的字尾與之對應,所以就不斷遞迴next陣列即可求解。
POJ2752程式碼

#include<stdio.h>
#include<iostream>
#include<string.h>
#include<stack>
using namespace std ;
const int maxn = 1e6+5;
int ans[maxn];
int Next[maxn];
char str[maxn];
char mo[maxn];
int n1,n2;
void GetNext()
{
    int i=0,j=-1;
    while(i<n2)
    {
        if(j==-1||mo[i]==mo[j]) {++i,++j,Next[i]=j;}
        else j=Next[j];
    }
    return ;
}
int main()
{
    int cnt;
    while(scanf("%s",mo)!=EOF)
    {
        cnt=0;
        n2=strlen(mo);
        Next[0]=-1;
        GetNext();
        int j=n2;
        while(j!=0)
        {
            ans[cnt++]=j;
            j=Next[j];
        }
        for(int i=cnt-1;i>=0;i--)
        {
            printf("%d%c",ans[i],i==0?'\n':' ');
        }
    }
    return 0;
}

KMP第八題

POJ3080
本題題意為求m個字串長度至少為3的最長公共子串
由於m只有10而且len小於60,我們可以選擇列舉某一個串的子串並用str.find()或者kmp驗證是否所有該子串在所有字串中出現過,也可以用經典的二分長度將height陣列分塊的字尾陣列做法
POJ3080(find解法

//由於只查詢是否出現過,演算法複雜度差距不大,所以這裡給出簡單一些的寫法
#include<stdio.h>
#include<algorithm>
#include<iostream>
#include<string.h>
using namespace std;
const int maxn = 65;
char str[maxn];
string ansstr;
string str2[maxn];
int main()
{
    int n,t;
    scanf("%d",&t);
    while(t--)
    {
        scanf("%d",&n);
        for(int i=0;i<n;i++)
        {
            scanf("%s",str);
            str2[i]=str;
        }
        int ans=0;
        string tmp=str2[0];
        int tmplen=tmp.size();
        for(int i=0;i<tmplen;i++)
        {
            for(int j=1;i+j<=tmplen;j++)
            {
                int cnt=1;
                string ss=tmp.substr(i,j);
                for(int k=1;k<n;k++)
                {
                    if(str2[k].find(ss)!=-1)
                        cnt++;
                }
                if(cnt==n)
                {
                    if(ans<j)
                    {
                        ans=j;
                        ansstr=ss;
                    }
                    else if(ans==j)
                    {
                        ansstr=min(ansstr,ss);
                    }
                }
            }
        }
        if(ans<3) printf("no significant commonalities\n");
        else printf("%s\n",ansstr.c_str());
    }
}

POJ3080(字尾陣列寫法

//具體實現原理可以參考我的字尾陣列部落格
#include <iostream>
#include<algorithm>
#include <stdio.h>
#include <string.h>
using namespace std;
#define maxn 4005
const int INF = 0x3f3f3f3f;
int wa[maxn],wb[maxn],wsf[maxn],wv[maxn],sa[maxn];
int rank[maxn],height[maxn],s[maxn];
char str[15][65];
int t,lenn[maxn];
int belong[maxn];
int anspos;
int vis[65];
int cmp(int *r,int a,int b,int k)
{
    return r[a]==r[b]&&r[a+k]==r[b+k];
}
void getsa(int *r,int *sa,int n,int m)
{
    int i,j,p,*x=wa,*y=wb,*t;
    for(i=0; i<m; i++)  wsf[i]=0;
    for(i=0; i<=n; i++)  wsf[x[i]=r[i]]++;
    for(i=1; i<m; i++)  wsf[i]+=wsf[i-1];
    for(i=n; i>=0; i--)  sa[--wsf[x[i]]]=i;
    p=1;
    j=1;
    for(; p<=n; j*=2,m=p)
    {
        for(p=0,i=n+1-j; i<=n; i++)  y[p++]=i;
        for(i=0; i<=n; i++)  if(sa[i]>=j)  y[p++]=sa[i]-j;
        for(i=0; i<=n; i++)  wv[i]=x[y[i]];
        for(i=0; i<m; i++)  wsf[i]=0;
        for(i=0; i<=n; i++)  wsf[wv[i]]++;
        for(i=1; i<m; i++)  wsf[i]+=wsf[i-1];
        for(i=n; i>=0; i--)  sa[--wsf[wv[i]]]=y[i];
        t=x;
        x=y;
        y=t;
        x[sa[0]]=0;
        for(p=1,i=1; i<=n; i++)
            x[sa[i]]=cmp(y,sa[i-1],sa[i],j)? p-1:p++;
    }
}
void getheight(int *r,int n)
{
    int i,j,k=0;
    for(i=1; i<=n; i++)  rank[sa[i]]=i;
    for(i=0; i<n; i++)
    {
        if(k)
            k--;
        else
            k=0;
        j=sa[rank[i]-1];
        while(r[i+k]==r[j+k])
            k++;
        height[rank[i]]=k;
    }
}
int check(int x,int n)
{
    for(int i=1;i<=n-1;i++)
    {
        if(height[i]<x) continue;
        int cnt=0;
        for(int j=0;j<=t;j++) vis[j]=0;
        while(height[i]>=x&&i<=n-1)
        {
            if(!vis[belong[sa[i-1]]])
            {
                vis[belong[sa[i-1]]]=1;
                cnt++;
            }
            i++;
        }
        if(!vis[belong[sa[i-1]]])
        {
            vis[belong[sa[i-1]]]=1;
            cnt++;
        }
        if(cnt>=t)
        {
            anspos=sa[i-1];
            return true;
        }
    }
    return false;
}
int main()
{
    int len,n;
    int casee;
    scanf("%d",&casee);
    while(casee--)
    {
        scanf("%d",&t);
        if(t==0) break;
        n=0;
        int pos=30;
        for(int i=0;i<t;i++)
        {
            scanf("%s",str[i]);
            lenn[i]=strlen(str[i]);
            for(int j=0;j<lenn[i];j++)
             {
                 s[n++]=str[i][j]-'A'+1;
                 belong[n-1]=i;
             }
            s[n++]=pos++;
        }
        s[n]=0;
        getsa(s,sa,n,pos);
        getheight(s,n);
        int l=1,r=60,mid;
        while(l<=r)
        {
            mid=(l+r)>>1;
            if(check(mid,n)) l=mid+1;
            else r=mid-1;
        }
        if(r<3) printf("no significant commonalities  \n");
        else
        {
            for(int i=anspos;i<anspos+r;i++)
                printf("%c",s[i]-1+'A');
            printf("\n");
        }
    }
    return 0;
}

KMP第九題

HDU2594
本題題意是求既是A串中的字首又是B串中的字尾的最長長度。
如果我們將AB進行拼接,我們可以發現next[len1+len2]即為最長的即使字首又是字尾的子串,但是這裡有一個細節,就是如果這個長度大於min(len1,len2),代表這個是拼接之後產生的,是不可取的,所以我們可以運用性質2來不斷減少這個長度,直到他滿足len<min(len1,len2)即可。
HDU2594程式碼

#include<stdio.h>
#include<iostream>
#include<string.h>
#include<stack>
using namespace std ;
const int maxn = 1e6+5;
int Next[maxn];
char str[maxn];
char mo[maxn];
int n1,n2;
void GetNext()
{
    int i=0,j=-1;
    while(i<n2)
    {
        if(j==-1||mo[i]==mo[j]) {++i,++j,Next[i]=j;}
        else j=Next[j];
    }
    return ;
}
int main()
{
    int cnt;
    while(scanf("%s%s",mo,str)!=EOF)
    {
        n1=strlen(str);
        n2=strlen(mo);
        int tmp=n2;
        for(int i=n2;i<n2+n1;i++)
        {
            mo[i]=str[i-n2];
        }
        n2=n1+n2;
        mo[n2]='\0';
        Next[0]=-1;
        GetNext();
        int ans=Next[n2];
        while(ans>min(tmp,n2-tmp))
        {
            ans=Next[ans];
        }
        if(ans==0)
        {
            printf("0\n");
            continue;
        }
        for(int i=0;i<ans;i++)
            printf("%c",mo[i]);
        printf(" ");
        printf("%d\n",ans);
    }
    return 0;
}

KMP第十題

HDU3336
本題題意為求字串的每個字首在整個字串中的出現次數。
我們想象性質二,如果next[j]對答案有一個貢獻,那麼這個貢獻在j中一定會再貢獻一次,而且j為結尾的字串對於總串產生的貢獻只有長度為j的子串,於是我們可以得到轉移方程ans[j]=ans[next[j]]+1,最後對所有字首的貢獻取和即為答案。
HDU3336程式碼

#include<stdio.h>
#include<iostream>
#include<string.h>
#include<stack>
using namespace std ;
const int maxn = 1e6+5;
int Next[maxn];
char str[maxn];
char mo[maxn];
int dp[maxn];
int n1,n2;
void GetNext()
{
    int i=0,j=-1;
    while(i<n2)
    {
        if(j==-1||mo[i]==mo[j]) {++i,++j,Next[i]=j;}
        else j=Next[j];
    }
    return ;
}
int kmp()
{
    int cnt=0;
    int i=0,j=0;
    while(i<n1)
    {
        if(j==-1||str[i]==mo[j]) i++,j++;
        else j=Next[j];
        if(j==n2)
        {
            cnt++;
            j=0;
        }
    }
    return cnt;
}
int ans[maxn];
int main()
{
    int cnt;
    int t;
    scanf("%d",&t);
    while(t--)
    {
        scanf("%d%s",&n2,mo);
        Next[0]=-1;
        GetNext();
        int ans=0;
        dp[0]=0;
        for(int i=1;i<=n2;i++)
        {
            dp[i]=dp[Next[i]]+1;
            ans=(ans+dp[i])%10007;
        }
        printf("%d\n",ans);
    }
    return 0;
}

KMP第十一題

HDU4300
本題題意比較難讀懂,題意為給你一段密文的對映方式和一段密文+明文的字串,密文是完整的,而明文不一定是完整的,讓你新增最少的字元使他變為完整的密文+明文
如果讀懂題意,可以考慮給定字串中密文長度一定是>=len/2的,所以我們可以將後半段的字元均按照對映換為密文,然後找到最長的既在字首中出現又在後綴中出現的子串,根據性質二和第九題的做法,不斷遞迴next直到找到符合條件的len然後跳出即可,輸出時要注意明文密文的轉換
HDU4300程式碼

#include<stdio.h>
#include<iostream>
#include<string.h>
#include<stack>
using namespace std ;
const int maxn = 1e5+5;
int Next[maxn];
char str[maxn];
char str2[maxn];
char mo[maxn];
int dp[maxn];
int mm[30];
int nn[maxn];
int n1,n2;
void GetNext()
{
    int i=0,j=-1;
    while(i<n2)
    {
        if(j==-1||mo[i]==mo[j]) {++i,++j,Next[i]=j;}
        else j=Next[j];
    }
    return ;
}
int ans[maxn];
int main()
{
    int cnt;
    int t;
    scanf("%d",&t);
    while(t--)
    {
        scanf("%s%s",str,mo);
        strcpy(str2,mo);
        n2=strlen(mo);
        n1=strlen(str);
        for(int i=0;i<n1;i++)
        {
            mm[i]=str[i]-'a';
            nn[str[i]-'a']=i;
        }
        for(int i=0;i<(n2+1)/2;i++)
        {
            mo[i]=nn[mo[i]-'a']+'a';
        }
        Next[0]=-1;
        GetNext();
        int ans=Next[n2];
        while(ans>min((n2+1)/2,n2-(n2+1)/2))
        {
            ans=Next[ans];
        }
        for(int i=0;i<n2-ans;i++)
            printf("%c",str2[i]);
        for(int i=0;i<n2-ans;i++)
            printf("%c",nn[str2[i]-'a']+'a');
        printf("\n");
    }
    return 0;
}

KMP第十二題

HDU1238
多個字串的最長公共子串,只不過子串可以逆置出現,只要把第八題的做法反過來再找一次就好,直接上程式碼

#include<stdio.h>
#include<algorithm>
#include<iostream>
#include<string.h>
using namespace std;
const int maxn = 105;
char str[maxn];
string ansstr;
string str2[maxn];
int main()
{
    int n,t;
    scanf("%d",&t);
    while(t--)
    {
        scanf("%d",&n);
        for(int i=0;i<n;i++)
        {
            scanf("%s",str);
            str2[i]=str;
        }
        int ans=0;
        string tmp=str2[0];
        int tmplen=tmp.size();
        for(int i=0;i<tmplen;i++)
        {
            for(int j=1;i+j<=tmplen;j++)
            {
                int cnt=1;
                string ss=tmp.substr(i,j);
                for(int k=1;k<n;k++)
                {
                    if(str2[k].find(ss)!=-1)
                        cnt++;
                }
                if(cnt==n)
                {
                    if(ans<j)
                    {
                        ans=j;
                        ansstr=ss;
                    }
                    else if(ans==j)
                    {
                        ansstr=min(ansstr,ss);