前言

爆肝一晚上,稍微摸着点烤馍片$KMP$的感觉。


正文

$KMP$算法,全名$Knuth-Morris-Pratt$算法,没错就是这仨神犇他们整出来的字符串匹配算法。

那么这个算法有什么用呢?

首先,这是一种模式串匹配算法,即在理论较长的文本串(下文以$txt$代称)中,求出一个模式串$pat$在其中出现的是否,位置,次数等。

那么先来解决一个小问题:

给出两个字符串$txt$与$pat$,其中$pat$是$txt$的子串,求出$pat$在$txt$中出现的所有位置。

假如$1≤|pat|≤|txt|≤5000$,这时我们考虑使用暴力求解就好。

只需要不断地在$txt$中按位匹配$pat$,如果不匹配就返回到$txt$上次开始匹配的下一位,$pat$从头开始即可。

//暴力匹配
int search(string pat,string txt)
{
    int lenp=pat.length;
    int lent=txt.length;
    for(int i=0;i<lent-lenp;i++)
    {
        int j;
        for(j=0;j<lenp;j++)
            if(pat[j]!=txt[i+j])
                break;
        //pat全都匹配了
        if(j==lenp)return i;
    }
    return -1;
}

但是,如果$1≤|pat|≤|txt|≤10^6$呢?

因为暴力匹配的时间复杂度达到了$O(n*m)$,如果进行强行暴力匹配的话,是会$T$飞的。

此时,我们就需要$KMP$出场了。

优化一个算法,首先要找到它在哪里耗费了过多的时间。下面的动图就是一个很好的演示。

brute.webp

我们可以看到,在失配之后,指针$i$依然回到了前一次的后一位开始,但是前面根本就没有字符 b,这就是时间的浪费。

而$KMP$算法聪明的是,它花费了一定的空间来存储失配后指针的去向,节省了一大段不断尝试失败的时间。比如像这样:

kmp1.webp

又或者像这样:

kmp2.webp

在这个例子中,因为$pat$中的前三个字符都与$txt$中的是匹配的,所以并没有必要像暴力算法那样回退指针,只需要比较字符 b是否匹配就好了。

在$KMP$算法中,文本串$txt$中的指针$i$不会回退,只通过$next$数组中存放的信息移动模式串$pat$中的指针,这样无论是什么字符串,都可以做到在$O(n)$完成匹配。需要注意的是,这个前缀数组$next$只和模式串有关,与文本串没有关系。

那么难点就在于怎么求得这个$next$数组。

在不同的状态遇到了不同的字符,作出的行为当然也是不同的,比如:

状态1.webp

所以,我们需要求的就是如果当前失配,最优选择是把模式串中的指针跳转到哪里

比如这样一个匹配过程:

匹配.gif

这就是KMP的核心逻辑

$KMP$ 的精髓在于,对于每次失配之后,我都不会从头重新开始枚举,而是根据我已经得知的数据,从某个特定的位置开始匹配;而对于模式串的每一位,都有唯一的“特定变化位置”,这个在失配之后的特定变化位置可以帮助我们利用已有的数据不用从头匹配,从而节约时间。

void getnext()
{
    int j=0;
    n=strlen(pat+1);
    for(int i=2;i<=n;i++)
    {
        while(j!=0&&pat[i]!=pat[j+1]){j=nxt[j];}
        //找到一个在第i位失配后能够重新进行匹配的状态j
        //如果nxt[j]还没有确认值,就返回至起始状态
        if(pat[i]==pat[j+1])j++;
        //如果找到,就第i位失配后跳转的位置赋值为j+1(跳到前一格)
        nxt[i]=j;
        //如果没找到,说明j已经跳回到状态0,赋值为j
    }
    return;
}

这段是求$next$数组的代码。

可以理解为将模式串进行自我匹配,每次确定至多一个next的值,最终会确定所有的next的值。

我们完全可以预处理出这样一个数组$next[j]$,表示当匹配到$pat$的第$j$个字母而第$j+1$个字母不能匹配了时,新的$j$最大是多少。$next[j]$应该是所有满足 pat[1~p[j]]=pat[j-p[j]+1~j]的最大值。简单点说,就是要找到从开头到第$j$位这一段的字符串中,前缀与后缀的最长公共部分,跳转时就可以考虑到因为这一段是有重叠的,所以可以直接把后缀匹配的部分替换为前缀匹配的部分,而文本串中的指针不回退,实现$O(n)$完成。

而匹配,就是把模式串对文本串进行匹配,代码和上面差不多;

void kmp()
{
    int j=0;
    int t=strlen(txt+1),p=strlen(pat+1);
    for(int i=1;i<=t;i++)
    {
        while(j!=0&&txt[i]!=pat[j+1]){j=nxt[j];}
        //失配时找到可以继续匹配的状态j 
        if(txt[i]==pat[j+1])j++;
        //如果匹配成功,模式串状态前进一步
        if(j==p)ans.push_back(i-p+1),j=nxt[j];
        //如果匹配完毕,记录答案,返回继续匹配 
    }
}

【模板】KMP字符串匹配

代码中的$i-p-1$即为匹配成功时模式串出现的位置,当然也可以直接输出。


文章图片来源见水印

最后修改:2020 年 11 月 06 日 11 : 17 PM