KMP算法

KMP算法

作用:用来处理字符串匹配问题

时间复杂度:ON

 空间复杂度:O(N)

 

思路:

假设以a[i]结尾的长度为j的字符串正好匹配b串的前j个字符,那么考虑a[i+1]b[j+1]的情况

  1. A[i+1]==B[j+1] --> ++j,if(j==m) 匹配成功.
  2. A[i+1]!=B[j+1] --> 适当减小j的值,使前b字符串前j个重新匹配。

 

 

最难的情况就是处理2的情况:

思考问题1—— 如何最小次数的减小j使他符合情况

前提已知条件——j越大越好(因为j越大说明我们后续要再匹配的(m-j)个字符越少),同时每次j减一再重新判断是否匹配效率无疑是十分低下的!(遇到一个新的不匹配情况最坏处理(j-1*(j-2)/2次)

 

解决方法:

我们可以发现,处理新的ji无关,只与b串本身有关(要找出新的j使bj个字符匹配,这当然与i无关,因为我们必须先找到能匹配的j的最大值再判断新的j能否使a[i+1]==b[j+1],否则再次减小j的值直到j=0

 

那么我们来深耕b字符串它本身的性质

我们可以预处理一个P数组,来储存当遇到不匹配的时候新的最大j是多少(即最长的相同前缀和后缀)

我们考虑是否能用递推的方法推P

  假设P[4]=2,说明b[1.....2]==b[3.......4],思考P[5]能否由P[4]推得

  P[5]=P[4]+1当且仅当b[3]==b[5],发现有b[4+1]==b[p[4]+1]

b[3]=b[5]呢? 思考一下P[5]能否由P[4]的情况所包含的子串得到,即是否有P[5]=P[P[4]]+1?

  我们换一种普遍的情况来思考为什么P[i]可以由P[i-1]的所包含的字串来计算

假设P[i]=j,P[j]=k --> 说明对于b字符串来说有(1)前j个前缀和后缀(i-j+1i)相等,(2)前k个前缀和后缀(j-k+1j)相等,结合两式子有前k个前缀和后缀(i-k+1i)相等,(类比情况2找最大j),则可以由子串来推得P

为什么预处理和KMP那么像? 因为它本身也是一种“自我匹配”

 

步骤3

当遇到j=0时仍然无法匹配a[i+1]b[j+1]怎么办???

我们直接增加i的值,直到找到匹配的为止

 

思考问题2——为什么最后的复杂度为ON),即考虑while循环的次数问题

我们可以发现,j每次符合的情况只能+1,则j增加的次数最多为n的级别,而while的作用是减小j使其重新匹配,减小次数最多也为n的级别(因为每次最少-1),则均摊后每次为O1)型。而且预处理的算法本身和匹配算法是一致的,时间复杂度为Om)。

则最后KMP算法为On)(m<n

 

 

例题1:hdu-2087

一块花布条,里面有些图案,另有一块直接可用的小饰条,里面也有一些图案。对于给定的花布条和小饰条,计算一下能从花布条中尽可能剪出几块小饰条来呢?

Sample

Input

Output

abcde a3

aaaaaa  aa

#

0

3

 

#include<bits/stdc++.h>
using namespace std;
char a[1005],b[1005];
int p[1005];
int m,n;

void pre(){
    p[1]=0;//也是一种限制条件,避免一直循环。
    int j=0;//表示前j个数是匹配的
    for(int i=1;i<m;i++){
        while(j>0&&b[j+1]!=b[i+1]) j=p[j];
        if(b[i+1]==b[j+1]) ++j;
        p[i+1]=j;//记得记录新的P
    }
}

int kmp(){
    int ans=0,j=0;
    for(int i=0;i<n;i++){
        while(j>0&&b[j+1]!=a[i+1]) j=p[j];//不能匹配且j还未到0的情况,若没有j>0这个条件,会陷入死循环
        if(b[j+1]==a[i+1]) ++j;
        if(j==m){
            ans++;
            j=0;//!!这里特别重要——若是所求字符串可以重复的情况下,为j=p[j],否则为这个
        }
    }
    return ans;
}
int main(){
    while(cin>>a+1){//表示输入的字符串串首为a[1]
        if(a[1]=='#') break;
        scanf("%s",b+1);
        m=strlen(b+1);
        n=strlen(a+1);
        pre();
        cout<<kmp()<<endl;
    }
    return 0;
}

 

例题2.Power Strings POJ - 2406 

Given two strings a and b we define a*b to be their concatenation. For example, if a = "abc" and b = "def" then a*b = "abcdef". If we think of concatenation as multiplication, exponentiation by a non-negative integer is defined in the normal way: a^0 = "" (the empty string) and a^(n+1) = a*(a^n).

Sample

Input

Output

abcd

aaaa

ababab

.

1

4

3

 

特别之处:

If(n%(n-p[n])==0) ans=n/(n-p[n])

如何理解? 我们看看能否推出一些条件(或许没有用),如果是连续重复,那么有p[n]>n/2,因为如果小于,那么p[n]个前缀和后缀是无法覆盖所有字符串的,因此是无法连续的。

对于一个字符串,我们先找出a^n中的那一小段a,通过发现,a正好就是a[n-p[n]+1]到a[n]。假设长度为20,p[20]=16,那么我们以分块的思想把它分成ABCDE五份,则有A=B,B=C,C=D,D=E,即A=B=C=D=E。正好有n/(n-p[n])=5

思考为什么是找p[n]呢,因为我们知道p[n]是最大的j是前后缀重新相同,而我们要求largest,就需要使所分的部分a尽可能小(也就是n-p[n]),p[n]最大的性质正好满足了

 

#include<iostream>
#include<cstdio>
#include<cstring>//strlen   <string>不行
using namespace std;
int p[1000006],n;
char a[1000006];

void pre(){
    int j=0;
    p[1]=0;
    for(int i=1;i<n;i++){
        while(j>0&&a[i+1]!=a[j+1]) j=p[j];
        if(a[i+1]==a[j+1]) ++j;
        p[i+1]=j;//!!!别写成p[j]=j
    }
}

int kmp(){
    int ans=0;
//    for(int i=1;i<=n;i++){//i的范围是1-n(可以取n)
//        if(i%(i-p[i])==0) ans=max(ans,i/(i-p[i]));
//    }//根据题目要求知道所求的是整个字符串的largest^n而非根据局部所求的maxans
    if(n%(n-p[n])==0) ans=n/(n-p[n]);
    else ans=1;
    return ans;
}

int main(){
    while(1){
        scanf("%s",a+1);
        if(a[1]=='.') break;
        n=strlen(a+1);
        pre();
        cout<<kmp()<<endl;
    }
    return 0;
}

 

例题3:Radio Transmission BZOJ - 1355 

给你一个字符串,它是由某个字符串不断自我连接形成的。 但是这个字符串是不确定的,现在只想知道它的最短长度是多少.

 

Sample

 

Input

Output

8
cabcabca

3

 

#include<bits/stdc++.h>
using namespace std;
int n,p[1000006];
char a[1000006];

void pre(){
    int j=0;
    p[1]=0;
    for(int i=1;i<n;i++){
        while(j>0&&a[i+1]!=a[j+1]) j=p[j];
        if(a[i+1]==a[j+1]) ++j;
        p[i+1]=j;
    }
}

int main(){
    cin>>n;
    scanf("%s",a+1);
    pre();    
    cout<<n-p[n]<<endl;//对于样例,可能会出错的使以为p[8]=2,其实仔细发现p[8]=5
    return 0;
}

 

 

似乎在梦中见过的样子 BZOJ - 3620

“Madoka,不要相信 QB!”伴随着 Homura 的失望地喊叫,Madoka 与 QB 签订了契约.

这是 Modoka 的一个噩梦,也同时是上个轮回中所发生的事.为了使这一次 Madoka 不再与 QB签订契约,Homura 决定在刚到学校的第一天就解决 QB.然而,QB 也是有许多替身的(但在第八话中的剧情显示它也有可能是无限重生的),不过,意志坚定的 Homura 是不会放弃的——她决定

消灭所有可能是 QB 的东西.现在,她已感受到附近的状态,并且把它转化为一个长度为 n 的字符串交给了学 OI 的你.

现在你从她的话中知道 , 所有形似于 A+B+A 的字串都是 QB 或它的替身 , 且len(A)>=k,len(B)>=1 (位置不同其他性质相同的子串算不同子串,位置相同但拆分不同的子串算同一子串),然后你必须尽快告诉 Homura 这个答案——QB 以及它的替身的数量.

 

Sample

 

Input

Output

8
cabcabca

3

 

 

#include<bits/stdc++.h>
using namespace std;
int k,p[15005],ans,num[15005];
char a[15005];
#define minn(a,b) (a > b ? b : a)
const int MAXN=0x3f3f3f;//0x3f会爆0
int main(){
    scanf("%s",a+1);
    cin>>k;
    int n=strlen(a+1);
    for(int l=1;l<=n-2*k;l++){
        int j=0;
        p[1]=0;//勿漏
        memset(num,MAXN,sizeof(num));//漏了就错
        for(int len=2;len<=n-l+1;len++){
            int r=l+len-1;
            while(j>0&&a[r]!=a[j+l]) j=p[j];//易错a[j+1](以为起始点变成l!! 
            //a[j+l+1]????——> 模板为a[i+1]!=a[j+1],初始i+1=2,j+1=1。而此处j+l相当于j+1,r相当于i+1
            if(a[r]==a[j+l]) ++j;
            p[len]=j;//p[i+1]=j
            if(j<k) num[len]=MAXN;//判断是否满足len(A)>=k的情况
            else num[len]=minn(num[p[len]],j);//引入num数组的作用是特判子串重叠的情况
            //如 abcabcabc 则p[9]=6, 但是可以令A.B皆为abc可构成,所以可以考虑类似于路径压缩的方法,记录最小元
            if(2*num[len]<len) ++ans;//保证len(B)>=1
            //(2*num[len]的情况) : abcabc 有num[6]=3 2*...==len(就是不满足len(B))
        }
    }
    cout<<ans<<endl;
    return 0;
}

 

 

 

 

posted @ 2023-05-30 18:55  比奇堡悍匪派大星  阅读(30)  评论(0编辑  收藏  举报