KMP

· · 个人记录

前言

不会画图 QwQ,有些图需要放大,请见谅。

前置知识

border

定义:在字符串 S 中,若有长度为 k 的前缀与长度为 k 的后缀相等,则称该前缀(后缀)为 S1\operatorname{border}

一个简单性质:字符串 S\operatorname{border}\operatorname{border} 也是 S\operatorname{border}

证明:

KMP 算法

算法介绍

用于快速查找字符串 TS 中的匹配位置。

先谈谈无优化的暴力解法,时间复杂度为 O(|S|*|T|)

KMP 可视作暴力算法的优化,但其时间复杂度仅有 O(|S|+|T|)

它是怎么优化的呢?假设我们要在 abcabcabf 中匹配 abcabf

回想暴力的流程,首先枚举点 i,然后依次判断 S_{i+j-1} 是否等于 T_j。实际上,这部分与 KMP 是大致相同的。

不过,KMP 枚举 i维护 j。代表 T_1 ~ T_jS_{i-j+1} ~ S_i 一一对应。

ST 的对应位不同(称为失配),暴力算法则退出循环,将 i 前进一位,但 KMP 则考虑运用已知条件,找到最优继续匹配位置。

定义失配数组 nxt[j] 表示 T 长度为 j 的前缀的最长 \operatorname{border}(的结尾指针)。

发生失配时,可以通过改变 j 的值使得 S_i=T_{j+1}。 因为 T_1 ~ T_{nxt[j]} = T_{j-nxt[j]+1} ~ T_j,所以令 j=nxt[j],这样既保证正确性又达到优化的效果。

$i=6$、$j=5$ 时,发生失配: (为啥我列表没了诶!) | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | | -----------: | -----------: | -----------: | -----------: | -----------: | -----------: | -----------: | -----------: | -----------: | | a | b | c | a | b | c | a | b | f | | 1 | 2 | 3 | 4 | 5 | | | | | a | b | c | a | b | | | | $nxt[4]=2$,将 $j$ 改为 $2$: | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | | -----------: | -----------: | -----------: | -----------: | -----------: | -----------: | -----------: | -----------: |-----------: | | a | b | c | a | b | c |a |b | f | | | | | 1 | 2 | | | | | | | | a | b | | | | 继续匹配,直到 $i=9$、$j=6$,$T$ 已被遍历完,于是匹配 位置为 $4$: | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | | -----------: | -----------: | -----------: | -----------: | -----------: | -----------: | -----------: | -----------: |-----------: | | a | b | c | a | b | c |a |b | f | | | | | 1 | 2 | 3 | 4 | 5 | 6 | | | | | a | b | c | a | b | f | 算法结束。 #### 失配数组的建立 可粗略理解为 $T$ 的自匹配,同样记录 $i$、$j$。 假设我们已经知道 $nxt_1$ ~ $nxt_{i-1}$ 的值,而 $j=nxt_{i-1}$,接下来怎么推出 $nxt_i$? 从定义入手,$nxt_{i-1}$ 告诉我们:$T_1$ ~ $T_j$ $=$ $T_{(i-1)-j+1}$ ~ $T_{i-1}$。 代数角度较难理解,画图就很清晰了: ![](https://cdn.luogu.com.cn/upload/image_hosting/asyxw1l5.png?x-oss-process=image/resize,m_lfit,h_170,w_225) 显然,只要绿色格子相同,即满足 $T_{j+1} = T_i$时,$nxt_i=j+1

假如不相同呢?为了方便表达,定义 z=nxt_j

\operatorname{border} 的性质可知,T_1 ~ T_z 也是 T_1 ~ T_{i-1} 的前缀的 \operatorname{border},用图来表示就是这样:

显然,满足 T_{z+1} = T_i 即可。

以此类推,底层原理就是如此。

代码实现时,我们不需要 z 等变量,不断令 j=nxt_j 即可。

代码模板

裸题

#include<string>
#include<cstring>
#include<cstdio>
#include<algorithm>

#define sc scanf
#define pr printf

using namespace std;
const int N=1e6+5;
int nxt[N],len1,len2;
char st1[N],st2[N];
void border()
{
    int j=0;
    nxt[1]=0;
    for(int i=2; i<=len2;i++) //注意从2开始枚举
    {
        while(j&&st2[j+1]^st2[i])
        {
            j=nxt[j];
        }
        if(st2[j+1]==st2[i])
        {
            ++j;
        }
        nxt[i]=j;
    }
}
void kmp()
{
    int j=0;
    for(int i=1; i<=len1;i++)
    {
        while(j&&st1[i]^st2[j+1])
        {
            j=nxt[j];
        }
        if(st1[i]==st2[j+1])
        {
            ++j;
        }
        if(j==len2)
        {
            pr("%d\n",i-len2+1);
            j=nxt[j]; //为了继续匹配
        }
    }
}
int main()
{
    sc("%s%s",st1+1,st2+1);
    len1=strlen(st1+1);
    len2=strlen(st2+1);
    border();
    kmp();
    for(int i=1; i<=len2;i++)
    {
        pr("%d ",nxt[i]);
    }
    return 0;
}

时间复杂度分析

先讨论 KMP 主函数的最坏时间复杂度。

首先,i 总共增加 |S| 次,j 只会随 i 增加而增加,所以 j 至多增加 |S| 次。

在最坏情况下,如 S=\{aaaa...\} 时,nxt_j=j-1,同时 j≥0。结合上述条件,可知 j 至多减少 |S| 次。

于是,主函数的时间复杂度是 O(|S|);同理,预处理部分的时间复杂度是 O(|T|)。所以总时间复杂度就是 O(|S|+|T|)

失配树

失配树模板

如上面这题,当题目是求两前缀的最长公共 \operatorname{border} 长度时怎么办?直接枚举会超时,这时失配树就出场了。

利用 \operatorname{border} 的性质,我们发现 S\operatorname{border} 集合可表示为 S 的最大 \operatorname{border} 与其 \operatorname{border} 集合。

因此,S\operatorname{border} 集合就是 nxt_{|S|}nxt_{nxt_{|S|}}nxt_{nxt_{nxt_{|S|}}}...... 诶,很像是树中父子节点的关系。

于是,对于每个点 i,建立一条由 nxt_i 指向 i 的边,所建成的树称作失配树。

失配树中,对于任意点 u,它的所有祖先节点就是它的所有 \operatorname{border}

回顾题目,易发现两前缀的最长公共 \operatorname{border} ,就是失配树上两结点的 LCA,本题解决。