第五章 BF算法和KMP算法

KMP算法练习题
https://vjudge.net/contest/196097

BF算法

#include<iostream>
#include<cstring>
using namespace std;

//时间复杂度O(n*m)
int BF_match(char *s,char *t){
    int len1=strlen(s),len2=strlen(t);
    if(len1<len2) return -1;
    for(int i=0;i<=len1-len2;i++){
        if(s[i]==t[0]){
            bool flag=1;
            int k=i;
            for(int j=1;j<len2;j++){
                if(t[j]!=s[++k]){
                    flag=0;
                    break;
                }
            }
            if(flag) return i;
            else continue;
        }
    }
    return -1;
}

int main()
{
    char *s="abceabcabcacbab",*t="abcab";
    cout<<s<<endl<<t<<endl;
    cout<<BF_match(s,t)<<endl;
    return 0;
}

KMP算法

KMP算法确实不好理解,下面我来总结一下,希望能够用浅显易懂的语言加上相应图示例,可以达到通俗易懂的目的!!!写的字比较多些,不怕读不懂,只怕没耐心!!!

首先,得说下KMP算法是干啥的!KMP算法是为了查找一个字符串(主串)中是否包含另一个字符串(子串),我们不妨把子串叫做模式串,BF(暴力解决模式匹配)和KMP就是所谓的模式匹配算法。
下面举一个例子来说这个算法,比如查找串BFABCABCABDE是否包含字符串ABCABD
刚开始的情形是:
主串:BFABCABCABDE
子串:ABCABD
发现子串第一个字母A和主串第一个字母B不匹配,则子串右移一位,变为:
主串:BFABCABCABDE
子串: ABCABD
发现子串第一个字母A和主串第二个字母B不匹配,则子串继续右移一位,变为:
主串:BFABCABCABDE
子串: ABCABD
就这样,不断右移一位,知道子串的第一个字母和主串开始匹配。然后比较子串的第二个及后续字母是否和主串相应字母匹配,发现子串的前五个字母ABCAB和主串都匹配,只有D不匹配(前面的几个字母都匹配,到D的时候开始发生失配),此时,该怎么移动子串继续比较呢?
此时的暴力想法(BF算法)是如下搜索过程(不断右移一位):
主串:BFABCABCABDE
子串: ABCABD

主串:BFABCABCABDE
子串: ABCABD

主串:BFABCABCABDE
子串: ABCABD
就这样不断右移一位,终于达到匹配。这是BF的算法,也是我们自然的想法,而KMP算法是什么呢?KMP算法的想法就是当失配的时候,能不能一次不止右移一位,能不能多移动几位呢?如果能,具体一次能右移多少位呢?右移的位数和什么有关呢?
先看示例,仍然以上面的例子来看,上面的匹配过程中,当达到下面的这种状态的时候:
主串:BFABCABCABDE
子串: ABCABD
我们采用了BF算法,如果我们采用KMP算法(暂时知道这个算法的核心内涵在一定程度上可以让子串多后移几位就可以了)的话是这样的:
主串:BFABCABCABDE
子串: ABCABD
直接后移了三位,实现了匹配,是不是比上面的BF算法要省时很多呀!
下面就来说,当子串的前几个字母已经匹配,遇到一个字母失配的情形下,字符串究竟要后移多少位的问题!
仍然用上面的例子,当失配的时候情形如下:
主串:BFABCABCABDE
子串: ABCABD
此时子串已经匹配的部分是ABCAB,子串有5个字母已经实现了匹配。现在就来回答上面的问题,当失配时,应该右移的位数仅仅和子串有关,说的再清楚些,当失配时,右移的位数仅仅和子串中出现重复一些字母有关(这也是KMP算法的核心,其实KMP算法就是利用了子串中有某些重复的片段,然后通过观察,推理,最后证明出当失配时,到底应该右移多少位的问题)。看上面的例子,当失配的时候,我们把子串右移了3位,达到了下面的效果:
主串:BFABCABCABDE
子串: ABCABD
我们能够猜想,右移三位和子串中包含了两个重复片段AB有着某种神秘的联系!这就引起了我们的研究兴趣,到底是什么样的关系呢?
下面慢慢引入,一步步证明。这里先引出前缀和后缀的概念。
字符串ABCABD的前缀如下:
ABCAB
ABCA
ABC
AB
A
字符串ABCABD的后缀如下:
BCABD
CABD
ABD
BD
D
通过这个例子,应该不用过多的解释就能明白什么是字符串的前后缀了!
上面的例子告诉我们,当子串的D字母失配时,我们后移了3位,我们就努力探索下这个3是怎么来的呢?这个3和字符串的前后缀有什么关系呢?
我们研究发现,当D失配时,子串已经完成匹配的部分ABCAB的前缀是:ABCA,ABC,AB,A后缀是:BCAB,CAB,AB,B。对比前后缀,我们发现,ABCAB的最长相同前后缀是AB,而AB的长度是2,ABCAB的长度是5,那么5-2=3,好神奇啊!原来后移3位是这么来的呀?这样的求解右移多少位的方法是通用的吗?怎么证明呢?
上面的例子中,右移位数=子串已经完成匹配片段(ABCAB)的长度-子串已经完成匹配片段(ABCAB)的最长公共前后缀(AB)的长度。右移多少位显然只和子串有关。下面证明这个例子的算法是通用的,是正确的。
我们首先设一个数组next(next[1]表示子串前一个字母的最长公共前后缀的长度,next[2]表示前两个字母的最长公共前后缀的长度,以此类推),我们不妨设上述字符串匹配到主串的第i+1个字符失配,子串的前i个已经匹配的字符片段ABCAB应该右移后保证其最长公共前后缀达到重合(即右移位数是已经匹配的子串长度减去next[i],这里的next[i]=2,即AB的长度),即如下示:
ABCAB
ABCAB
请看下图:
《第五章 BF算法和KMP算法》

我们不妨设主串是a,子串是b,当在a[i+1]处失配时,我们向右移动x=已经匹配的位数(ABCAB的长度5)-已经匹配的片段的最长公共前后缀(AB)的长度2=3。这里y=next[j]。这一点请特别注意!!!我们开始继续说,在上述移动后的基础上,如果发现a[i+1]和b[y+1]不匹配,则我们继续如下移动:
《第五章 BF算法和KMP算法》

首先是不是和上一幅图很类似???是吧!当然是!此时显然有如下关系:
yy=next[next[j]]=next[y](哇塞,这不就是递归吗!)
xx=next[j]-yy
如果这次移动后,主串的a[i+1]仍然和子串的b[yy+1]不匹配,我们就重复上述过程;如果a[i+1]和b[yy+1]匹配,则yy加1,继续往后比较。
上述证明过程,其实是描述了用子串去匹配主串的过程,其实我们完全套用上述过程,用子串去匹配子串,就是求解next数组的过程(我们可以这么做:用作主串的子串从下标1,即第二个字符开始,而用作子串的子串从下标0开始,有了上面那么多的讲解的基础,好好想想,这样做的话,不就是一直在比较一个子串的前缀和后缀的吗?!这一点好好想想吧!)。上述是为了叙述方便,我们把数组的开始下标都用1来说明问题,下面我们把数组开始的下标调成0,用代码实现求解next数组及KMP算法实现的过程。

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;

void make_next(char s[],int next[]){
    next[0]=0;
    for(int k=0,i=1;i<strlen(s);i++){
        while(k>0 && s[i]!=s[k]){k=next[k-1];}
        if(s[i]==s[k]){k++;}
        next[i]=k;
    }
}

int kmp(char s1[],char s2[],int next[]){
    make_next(s2,next);
    for(int i=0,k=0;i<strlen(s1);i++){
        while(k>0 && s1[i]!=s2[k]){k=next[k-1];}
        if(s1[i]==s2[k]){k++;}
        if(k==strlen(s2)) return i-strlen(s2)+1;
    }
    return -1;
}

int main()
{
    char s1[]="BFABCABCABDE";
    char s2[]="ABCABD";
    int next[10];
    cout<<kmp(s1,s2,next)<<endl;
    return 0;
}
#include<iostream>
#include<string>
using namespace std;

void make_next(string s,int next[]){
    next[0]=0;
    for(int k=0,i=1;i<s.length();i++){
        while(k>0 && s[i]!=s[k]){k=next[k-1];}
        if(s[i]==s[k]){k++;}
        next[i]=k;
    }
}

int kmp(string s1,string s2,int next[]){
    make_next(s2,next);
    int cnt=0;
    for(int i=0,k=0;i<s1.length();i++){
        while(k>0 && s1[i]!=s2[k]){k=next[k-1];}
        if(s1[i]==s2[k]){k++;}
        if(k==s2.length()) return i-s2.length()+1;
    }
    return -1;
}

int main()
{
    ios::sync_with_stdio(false);//用流方式输入的时候,这样可以加速,
                                //但是,如果对速度要求比较高,不要用流方式,用scanf,printf
    int next[10005];
    string s1,s2;
    cin>>s1>>s2;
    cout<<kmp(s2,s1,next)<<endl;
    return 0;
}

用这个算法的时候一定注意一个问题:不要定义next名字的数组,这和C++语言本身有next,避免有些编译器编译不过!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

补充一(用next找字符串的循环节)

例如,设字符串str=p1p2p3p4p5p6,next[6]=4,则有p1p2p3p4=p3p4p5p6,即p1p2=p3p4,p3p4=p5p6,这就是所求的循环节。
例题:A – Power Strings POJ – 2406

补充二(用next求最小覆盖矩阵)

问题:row*col的字符串矩阵,用最小的面积的字符串矩阵去覆盖它,求最小的字符串矩阵的面积(D – Milking Grid POJ – 2185 )

对于一般的一维字符串,不妨设长度是s,最小覆盖字符串显然是s-next[s-1](我们代码实现中,数组下标都是从0开始记)。那么对于二维的字符串矩阵,我们可以把每一列看成一个字符,求出第一个结果col-next1[col-1],第二个结果是row-next2[row-1]
显然,这两个结果的乘积就是要求的最小字符串矩阵

    原文作者:KMP算法
    原文地址: https://blog.csdn.net/ccnuacmhdu/article/details/78412374
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞