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 是很有帮助的。
- 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\) - 最长 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)\)。
伪代码
太折磨了,再也不写伪代码了。
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 树
我们可以有 \(i \to \pi (i)\) 连边,建成一棵树,即 Border 树(也叫做 fail 树/失配树)。这棵树的根为 \(0\),其它每个结点与前缀构成一一映射。这棵树也有一些优良的性质。
- 一个前缀串所有的 Border 是其在失配树上的所有祖先表示的串(\(0\) 除外)。
- 两个前缀的公共最长 Border 是一般是其 LCA(最近公共组件)表示的串。特别地,若 LCA 为其中某一个,则由于 Border 定义一般不包括本身,为 LCA 的父节点表示的串。
- 有前缀 \(S_{[1,x]}\) 为 Border 的前缀为 \(x\) 的子树(自身除外)。
- 一个前的 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\) 需要指定(因题而异),是明确的。
转移函数
起始状态 \(q_0=0\)。
接受状态的集合 \(F\) 需要根据题目来确定。
可以直接将 \(\delta\) 函数打表,实现严格 \(\Theta (1)\) 转移(原来是均摊 \(\Theta (1)\))。
举个例子,如果我需要的自动机是 \(S\) 是否在出现过,我们可以定义 \(Q=\{0,1,\dots,n\}\),略改 \(\delta\) 函数即可。
而接受状态集合 \(F=\{n\}\)。
题目
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]}\),则有转移
建立 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\) 删去其它路径上的即可。

浙公网安备 33010602011771号