【字符串】KMP - 匹配字符串

【匹配字符串S与T,判断T是否为S的子串】

 

即在主串S中快速匹配是否存在一个子串等同于模式串T

 

S为被匹配串(主串),T为匹配串(模式串)

 

实现方式:

 

在最普通的算法中,我们总是拿两个指针指向两个字符串的不同位置来匹配

 

 

 

如果某个字符匹配成功就把两个光标同时移动到下一个位置,即

 

 

 

如果不匹配,那么模式串就该整体右移一位,重复上述方式进行匹配

模式串的光标要返回模式串的第一个位置,主串的光标移动到移动后模式串开头对应的主串的位置,即

 

 

 

然后再继续匹配下去,直到模式串中的光标到达模式串的末尾,且最后一个字符也能匹配时(说明在原串中找到了一个子串等同于模式串),此时就能作为答案输出

 

 

 

但是,每一次不匹配就必须回溯光标到模式串开头对应,然后模式串整体往右移动一位,再继续匹配下去

 

为了能够做到O(n)时间内完成匹配,可以从这两点入手,去优化这一匹配的过程

 

引入Next数组概念,Next数组是从模式串中获得的

 

Next[i] 表示现在模式串已经匹配到了第 i-1 位,但是第 i 位的字符与原串不匹配

 

 

 

可以发现已匹配的串abcab中,前缀ab等于后缀ab

所以抓住这个特征,下一步就可以直接让模式串右移到后缀位置

 

 

 

可以发现移动后后缀位置是已经完成匹配的,所以原串的光标位置不变,继续匹配下去(O(n)的原因)

 

如果这样还是不能匹配,继续抓住已匹配的串ab继续右移下去

发现ab没有前缀等于后缀的情况出现(前缀后缀不能等于原串)

 

说明模式串只能直接移动到此时不匹配的位置上继续下去,即

 

所以求出Next数组是关键

void getNext(){
    int i,j=-1;
    Next[0]=-1;
    for(i=1;i<Tlen;i++){
        while(j>-1 && T[ j+1 ]!=T[ i ])
            j=Next[j];
        if(T[j+1]==T[i])
            j++;
        Next[i]=j;
    }
}

 

Next数组就是寻找前 i 个字符 前缀=后缀 的最长长度,且前缀=后缀≠原子串

在执行过程中,有以下几个例子://下标从0开始

    对于aba,在查找ab时可以得知Next[1]=-1,此时j=-1,所以i=2时对比的是j+1=0,发现a=a成立,所以j=0,Next[2]=0

       再比如aaaaa,在查找aaaa时得知Next[3]=2 , j=2 所以i=4时对比的是j+1=3,发现a=a成立,j++,Next[4]=j=3

       再比如aaaab,在查找aaaa时得知Next[3]=2 , j=2 所以i=4时对比的是j+1=3,发现a=b不成立,j=Next[j]=Next[3]=2,对比i=4,j=2不成立,继续,直到j=-1,所以最后Next[4]=-1

 

然后到应用部分

int KMP_Position(){
    int i,j=-1;
    for(i=0;i<Slen;i++){
        while(j>-1&&T[j+1]!=S[i])
            j=Next[j];
        if(T[j+1]==S[i])
            j++;
        if(j==Tlen-1)
            return i-Tlen+1;//找到后返回位置
    }
    return -1;//没找到
}

 

其中,对于下面这一句

 

while(j>-1&&T[j+1]!=S[i])
    j=Next[j];

 

这句说明在第 i 个位置原串S与模式串T失配了,但是由上面可以得知我们不需要回溯原串的光标i,只需要回溯模式串光标 j 即可,而j=Next[j] 就是指应该回溯到哪个位置(指模式串应该右移到什么位置),然后继续匹配此时的原串 i 位置和模式串 j 位置,直到完全不匹配时(j = -1)或者找到匹配位置(T[j+1] != S[i] )时退出这个循环

 

if(T[j+1]==S[i])
    j++;

这句判断跳出while循环的条件是不是由匹配才退出的,如果是匹配的话,j可以+1

 

然后记录此时Next[i] 的值

 

如果j==Tlen-1 成立,说明模式串的光标已经到了模式串的末尾,且最后一个位置也是匹配的,所以此时就能返回答案位置了—— i -Tlen + 1

 

另外,KMP还能快速查找模式串在原串中出现的次数,只要把输出条件更改成

if(j==Tlen-1){
    cnt++;
    j=Next[j];
}

 

然后用cnt作为答案即可

 

 

完整程序:

#include<bits/stdc++.h>
using namespace std;

string S,T;
int Slen,Tlen,Next[1000050];

void getNext(){
    int i,j=-1;
    Next[0]=-1;
    for(i=1;i<Tlen;i++){
        while(j>-1&&T[j+1]!=T[i])
            j=Next[j];
        if(T[j+1]==T[i])
            j++;
        Next[i]=j;
    }
}//模式串T的Next数组预处理

int KMP_Position(){
    int i,j=-1;
    for(i=0;i<Slen;i++){
        while(j>-1&&T[j+1]!=S[i])
            j=Next[j];
        if(T[j+1]==S[i])
            j++;
        if(j==Tlen-1)
            return i-Tlen+1;
    }
    return -1;
}//匹配模式串第一次出现在主串中的位置

int KMP_Count(){
    int i,j=-1,cnt=0;
    for(i=0;i<Slen;i++){
        while(j>-1&&T[j+1]!=S[i])
            j=Next[j];
        if(T[j+1]==S[i])
            j++;
        if(j==Tlen-1){
            cnt++;
            j=Next[j];
        }
    }
    return cnt;
}//匹配模式串在主串中出现的次数

int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);cout.tie(0);
    cin>>S>>T;
    Slen=S.size();
    Tlen=T.size();
    getNext();
    //cout<<KMP_Position()<<'\n';
    //cout<<KMP_Count()<<'\n';
    
    return 0;
}

 

 

 

 

 

附:

因为KMP算法的 getNext 函数求的是T字符串的前 i 个字符最长的 前缀=后缀 的长度,且这个长度不等于自身长度

所以如果求出的 Next[ LEN ] 满足

LEN % ( LEN - Next[ LEN] ) == 0

就可以说明这个字符串是由某个子串循环 LEN / ( LEN - Next[ LEN] )  次得到的

且这个子串是最短循环子串

也就是说, LEN / ( LEN - Next[ LEN] )  是字符串子串中最多的循环次数

例题1 POJ 2406

题目求的就是某个子串在整个字符串中循环的最多次数

例题2 POJ 1961

求的是所有循环次数大于等于 2 的循环节的循环次数

 

posted @ 2020-03-16 20:11  StelaYuri  阅读(207)  评论(0编辑  收藏  举报