KMP 与 Border 理论

KMP 与 Border 理论

约定见 字符串笔记

当机房中其他人都在学后缀数组时,我在学 KMP

KMP 是一个又难又简单的算法。[1]该算法由 Knuth、Morris、Pratt 发明。

KMP 从 Border 入手是我认为比较好理解的。

Border 是什么

Border 是定义为一个字符串的公共前后缀,即若 \(S_{[1,i]}=S_{[n-i+1,n]},i \lt n\),则称 \(S_{[1,i]}\)\(S\) 的一个 Border。[2]一个串的 Border 在定义中一般不包括它本身,否则有些性质就丧失了。

Border 有很多优良的性质。

先是两个和 Border 本身相关的性质,这对求 Border 是很有帮助的。

  1. Border 的 Border 是 Border
    证明:设 \(S_{[1,m]}\)\(S\) 的 Border,\(S_{[1,k]}\)\(S_{[1,m]}\) 的 Border,则 \(S_{[1,k]}=S_{[k-m+1,m]},S_{[1,m]}=S_{[n-i+1,n]}\),二者结合可以推出 \(S_{[1,k]}=S_{[k-m+1,m]}=S_{[n-k+1,n]}\),即 \(S_{[1,k]}\)\(S\) 的 Border。\(\blacksquare\)
  2. 最长 Border 的最长 Border 是次长 Border
    证明:设 \(S_{[1,m]}\)\(S\) 的最长 Border,\(S_{[1,k]}\)\(S\) 的一个非最长 Border。则 \(S_{[1,k]}=S_{[n-k+1,n]}\),\(S_{[1,m]}=S_{[n-m+1,n]}\),则 \(S_{[m-k+1,m]}=S_{[n-k+1,n]}=S_{[1,k]}\),即 \(S_{[1,k]}\)\(S_{[1,m]}\) 的 Border。这也就说明了,一个字符串非最长 Border 是最长 Border 的 Border\(\blacksquare\)

有这两个性质,我们可以知道 Border 是一个层序关系。

由此我们可以引出 KMP 算法。基于 KMP 算法,可以求 Border。

前缀函数

前缀函数是 KMP 算法的一个重要的概念。

我们定义前缀函数 \(\pi(i)\) 为长为 \(i\)前缀的最长 Border 的长度。特别的,我们令 \(\pi(1)=0\)。在代码中,一般写做 \(\mathrm{next}\)[3]

有上面的性质,我们可以知道求一个前缀的所有 Border。

如何求 \(\pi(i)\)?很容易想到一个如下的办法:从长到短依次枚举 \(i-1\) 的 border,如果能匹配下一个字符就更新 \(\pi(i)\)

复杂度如何?乍一看是 \(\mathcal O(n^2)\) 的,但实际上我们可以分析除更低的复杂度。定义势能函数 \(\Phi\) 为以 \(i\) 为前缀的字符串的 Border 数量。则每次 \(i\) 自增时要么花 \(\Theta(1)\) 的时间使 \(\Phi \gets \Phi +1\),要么花费 \(\Theta(\Delta \Phi)\) 的时间跳 \(\mathrm {next}\),故总复杂度为 \(\Theta (n)\)

伪代码

\[\begin{array}{ll} 1 & \textbf{Input. } \text{ a string }S\\ 2 & \textbf{Output. } \pi(i) \text{ of } S\\ 3 & \textbf{Method. }\\ 4 & n \gets |S| \\ 5 & \pi(1) \gets 0\\ 6 & \textbf{for } i \gets 2 \textbf{ to } n \\ 7 & \qquad p \gets \pi(i-1) \\ 8 & \qquad \textbf{while } p \not = 0 \\ 9 & \qquad \qquad \textbf{if } S[p+1] = S[i] \\ 10 & \qquad \qquad \qquad \textbf{break.}\\ 11 & \qquad \qquad p \gets \pi(p)\\ 12 & \qquad \textbf{if } S[p+1] = S[i] \\ 13 & \qquad \qquad \pi(i) \gets p +1 \\ 14 & \qquad \textbf{else} \\ 15 & \qquad \qquad \pi(i) \gets 0 \end{array} \]

太折磨了,再也不写伪代码了。

C++ 代码
void getnxt(){
    nxt[1]=0;
    for(int i=2;i<=n;i++){
        int p=nxt[i-1];
        while(p){
            if(S[p+1]==S[i]) break;
            p=nxt[p];
        }
        if(S[p+1]!=S[i]) nxt[i]=0;
        else nxt[i]=p+1;
    }
}

单模匹配

KMP 算法的重要用途是做单模匹配。没错,在此之前我们还没有说 KMP 算法

单模匹配,即给定一个文本串 \(S\) 和模式串 \(T\)\(n=|S|,m=|T|\),求模式串 \(T\) 在文本串中是否出现,出现了几次。

显然有一个暴力做法:从文本串 \(S\) 的第一个字符和模式串 \(T\) 的第一个字符开始经行比较,一旦匹配失败回退到 \(T\) 的第一个字符,复杂度最坏 \(\mathcal O(nm)\)

考虑用前缀函数优化这个过程。我们可以求出模式串的前缀函数,如果这一位不匹配,就跳到模式串前缀最长的 Border,直到匹配或前缀为空任然没有匹配。类似求前缀函数的复杂度分析,令 \(\Phi\) 为模式串中目前已经匹配的前缀字符串的 Border 数量,匹配上使 \(\Phi\) 自增,跳 Border 时 \(\Phi\) 自减,而文本串上的指针是单调的,故总时间复杂度为 \(\Theta (n+m)\),总空间复杂度为 \(\Theta (n)\)(用于存前缀函数)。

C++ 代码实现

此处是直接将所有匹配到的未知直接输出。

void find(){//需要先求出 T 的前缀函数 nxt
    int j=0;
    for(int i=1;i<=n;i++){
        while(j){
            if(T[j+1]==S[i]) break;
            j=nxt[j];
        }
        if(T[j+1]==S[i]) j++;
        if(j==m) printf("%d\n",i-m+1);
    }
}

此外另一种做法是将新建一个串为 \(T + \texttt{\#} + S\)\(+\) 表示拼接,\(\texttt{\#}\) 表示一个在 \(\Sigma\) 未出现的字符,再求前缀函数即可,\(\pi(i)=m\) 的位置即文本串中匹配到的位置。

Border 树

模板:P5829 【模板】失配树

我们可以有 \(i \to \pi (i)\) 连边,建成一棵树,即 Border 树(也叫做 fail 树/失配树)。这棵树的根为 \(0\),其它每个结点与前缀构成一一映射。这棵树也有一些优良的性质。

  1. 一个前缀串所有的 Border 是其在失配树上的所有祖先表示的串(\(0\) 除外)。
  2. 两个前缀的公共最长 Border 是一般是其 LCA(最近公共组件)表示的串。特别地,若 LCA 为其中某一个,则由于 Border 定义一般不包括本身,为 LCA 的父节点表示的串。
  3. 有前缀 \(S_{[1,x]}\) 为 Border 的前缀为 \(x\) 的子树(自身除外)。
  4. 一个前的 Border 数量为该点在 Border 树上深度减一。

这几个个性质是比较显然的。这也就是说 Border 约等于 Border 树上的祖先。

此外还有一些性质:

  • 一个前缀在串 \(S\) 中出现次数为次数为 Border 树中该点子树的大小。
    证明:注意到如果 \(S_{1,k}=S_{l,r},l>1\),则 \(S_{1,k}\)\(S_{1,r}\) 的一个 Border。故在 Border 树上,\(r\)\(k\) 的子树中。\(\blacksquare\)

写了一个很神奇的无递归树剖求 LCA。代码稍微有点长,所以放

周期(period)

周期亦称循环节。

定义正整数 \(k\)\(S\) 的周期,当且仅当 \(1\le k < |S|\)\(\forall k \lt i \le n,S_{i}=S_{i-k}\)

周期这个概念非常形象,正如其名,将一个前缀复制若干遍,截掉多余的,如果剩下的与原串相同,就是周期。

如果 \(k \mid |S|\)\(k\)\(S\) 的周期则称 \(k\)\(S\) 的整周期。

周期与 Border 相关:

  • \(k\)\(S\) 的周期等价于 \(S_{[1,n-k]}\)\(S\) 的 Border。
    这是个非常显然的结论,简单证一下:充分性:由周期的定义可以知道,\(S_{[k+1,n]}= S_{[k+1-k,n-k]} =S_{[1,n-k]}\)。必要性:由 Border 的定义可知,\(S_{[1,n-k]}=S_{[k+1,n]}\),即 \(\forall k+1 \le n,S_{i} = S_{i-k}\),所以 \(k\)\(S\) 的周期。(约等于把定义写了两遍)\(\blacksquare\)

由这个性质,我们可以简单地做这个题:P3435 [POI 2006] OKR-Periods of Words - 洛谷

Weak Periodicity Lemma:如果 \(p\)\(q\) 均为 \(S\) 的周期,且 \(p+q \le |S|\),则 \(\gcd (p,q)\) 也是 \(S\) 的周期。

证明:\(p=q\) 时显然成立。不妨假设 \(p>q\),令 \(d=p-q\),分类讨论。

  • \(i \gt q\) 时,\(S_{i} = S_{i-q} = S_{i-q+p}\)
  • \(i \le |S|-p\) 时,\(S_{i} = S_{i+p}= S_{i+p-1}\)

\(i \le q\)\(i \gt |S|-p\)\(i\) 一定不存在,(否则 \(q+p >|S|\)),故 \(d\)\(S\) 的周期。令 \(d\)\(q\) 一起辗转相减可以得到 \(\gcd (p,q)\)\(S\) 的周期。\(\blacksquare\)

此外还有更强的 Periodicity Lemma(如果 \(p\)\(q\)\(S\) 的周期,且 \(p+q \le |S| + \gcd (p,q)\),则 \(\gcd (p,q)\)\(S\) 的周期),但我不会证。

Border 的结构

TODO;太黑题了。

KMP 自动机

什么是自动机?这里不做定义,详见我的 形象地,可以理解为一张有向图,每条个点有 \(|\Sigma|\) 条出边,类似 Trie 树沿给定的字符走(实际上 Trie 树本身就是一种有限状态自动机)。

KMP 的结构亦可构成确定性有限状态自动机。可以称之为 KMP 自动机。

KMP 自动机可以看作 AC 自动机的一个特殊版本,即只有一个模式串的版本。

有限状态集合可以直接定义为 \(Q=\{0,1,\dots,n\}\),表示现在已经匹配了模式串的前缀 \(S_{[1,q]},q \in Q\)

字符集 \(\Sigma\) 需要指定(因题而异),是明确的。

转移函数

\[\delta (i,c) = \begin{cases} i+1 , S_{i+1}=c\\ 0,i=0 \wedge S_1 \neq c\\ \delta (\pi(i),c), \text{otherwise} \end{cases} \; (i \in Q,c \in \Sigma) \]

起始状态 \(q_0=0\)

接受状态的集合 \(F\) 需要根据题目来确定。

可以直接将 \(\delta\) 函数打表,实现严格 \(\Theta (1)\) 转移(原来是均摊 \(\Theta (1)\))。

举个例子,如果我需要的自动机是 \(S\) 是否在出现过,我们可以定义 \(Q=\{0,1,\dots,n\}\),略改 \(\delta\) 函数即可。

\[\delta (i,c) = \begin{cases} n , i =n\\ i+1 , S_{i+1} = c\\ 0,i=0 \wedge S_1 \neq c\\ \delta (\pi(i),c), \text{otherwise} \end{cases} \]

而接受状态集合 \(F=\{n\}\)

题目

P2375 [NOI2014] 动物园

链接P2375 [NOI2014] 动物园 - 洛谷

题意:求 \(S\) 的所有前缀 \(S_{[1,i]}\) 长度小于等于 \(\frac i 2\) 的 Border 的数量(记为 \(\mathrm{num}_i\))。

第一反应是一个这样的做法:先求出 \(S\) 的前缀函数,建 Border 树,在 Border 树上倍增,求出最长的长度小于等于 \(\frac i 2\) 的 Border。复杂度 \(\mathcal O (n \log n)\)。由于这是十几年前的题,现在可以轻松通过的,当时可能会被卡常。

考虑一个线性做法:假设现在已经处理了前缀 \(S_{[1,i]}\),现在要处理 \(S_{[1,i+1]}\)。我们维护一个指针 \(j\)(实现时不一定是真的指针),指向小于 \(\frac i 2\) 的最长的串。则 \(j\) 如果下一位匹配就自增,在向前跳到最长的长度小于等于 \(\frac i 2\) 的前缀即可。再维护 \(\mathrm{dep}\) 一样可以获得 \(\mathrm{num}\)

P3193 [HNOI2008] GT考试

链接P3193 [HNOI2008] GT考试 - 洛谷

题意:求 \(n\) 位长的字符串不包含子串长为 \(m\) 的子串 \(T\) 的方案数,对非固定的数取模,字符集 \(\Sigma\) 固定为十进制数字(\(\{0,1,\dots,9\}\)),\(n \le 10^9,m \le 20\)

\(n\le 10^9\) 太古怪了,又是一个和 \(n\) 关系很大的题且不大像有关于 \(n\) 较低复杂度的式子,那么大概率就是矩阵优化 DP 了。

先设计一个暴力的 DP 方案:设 \(f_{i,j}\) 表示匹配到主串的第 \(i\) 位,模式串 \(T\) 的第 \(j\) 位,转移特判 \(j=m\) 不能走即可。

具体地,假设在 \(j\) 后面加一个字符 \(c\) 能匹配到 \(T_{[1,p_c]}\),则有转移

\[f_{i,j} \to f_{i+1,p_c} [p_c \neq m](c \in \Sigma) \]

建立 kmp 自动机以创建转移矩阵,在由初状态 \(f_{0,0}=1\) 转移即可。

P3426 [POI 2005] SZA-Template

链接P3426 [POI 2005] SZA-Template - 洛谷

题意不大好概括的。

观察样例解释

ababbababbabababbabababbababbaba
ababbaba
     ababbaba
            ababbaba
                   ababbaba
                        ababbaba

可以发现,印章一定是原串的一个 Border。

继续观察可以发现若该 Border 是某个前缀的 Border,那么该串一定可以在该前缀再次印刷。

这就找到了判断某个串是否可以印刷的办法:一个串可做印章,当且仅当其在 Border 树中所有的顶点的编号的 max-gap[4] 小于等于串的长度。

这是一个简单的数据结构问题,发现支持删数是相对容易的,而删数是困难的,用双向链表实现,从 \(0\) 依次在 Border 树上检验到 \(n\) 删去其它路径上的即可。


  1. 这么说是因为 KMP 算法实现非常简单,很多人都背了板子,但是并不了解原理,稍微变一点形就不会了。此外,很多人不能一遍 AC KMP 模板题。在洛谷上本题从“普及-”一路升到“普及+/提高”。 ↩︎

  2. 有些定义中会将 Border 定义为长度,差别不大,无伤大雅。 ↩︎

  3. 有些人会写做 \(\mathrm{fail}\)(失配指针),这是借用了 AC 自动机的说法,也是等价的。还有一个原因是 next 会与标准库中命名重复,所以不能直接用 next 做变量名。 ↩︎

  4. 指排序后相邻元素差的最大值。 ↩︎

posted @ 2026-04-24 18:52  MZMTab  阅读(18)  评论(0)    收藏  举报