<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>KMP |</title><link>https://sichen-wang.github.io/tags/kmp/</link><atom:link href="https://sichen-wang.github.io/tags/kmp/index.xml" rel="self" type="application/rss+xml"/><description>KMP</description><generator>HugoBlox Kit (https://hugoblox.com)</generator><language>en-us</language><lastBuildDate>Sat, 24 Jun 2023 11:00:56 +0000</lastBuildDate><image><url>https://sichen-wang.github.io/media/icon_hu_9dfcb674aa885f38.png</url><title>KMP</title><link>https://sichen-wang.github.io/tags/kmp/</link></image><item><title>KMP 算法 &amp; KMP 自动机</title><link>https://sichen-wang.github.io/blog/kmp-%E7%AE%97%E6%B3%95-kmp-%E8%87%AA%E5%8A%A8%E6%9C%BA/</link><pubDate>Sat, 24 Jun 2023 11:00:56 +0000</pubDate><guid>https://sichen-wang.github.io/blog/kmp-%E7%AE%97%E6%B3%95-kmp-%E8%87%AA%E5%8A%A8%E6%9C%BA/</guid><description>&lt;h2 id="border--pi-函数"&gt;Border &amp;amp; pi 函数&lt;/h2&gt;
&lt;h3 id="一些定义"&gt;一些定义&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;字符串 \(S\) 的真前/后缀为非自身的前/后缀。&lt;/li&gt;
&lt;li&gt;字符串 \(S\) 的 border为 \(S\) 的公共真前/后缀。&lt;/li&gt;
&lt;li&gt;字符串 \(S\) 的最长 border 为 \(\pi\)，对于 \(S\) 的每个前缀 \(S_{1 \sim i}\) 令 \(\pi_{i}\) 为其最长 border，\(\pi\) 函数就是所谓的前缀函数。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="pi-函数的性质"&gt;pi 函数的性质&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;\(\forall i,\pi_{i+1}\leqslant \pi_{i}+1\)。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote class="border-l-4 border-neutral-300 dark:border-neutral-600 pl-4 italic text-neutral-600 dark:text-neutral-400 my-6"&gt;
&lt;p&gt;由反证法易知。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;字符串 \(S\) 的所有 border 可以由 \(\pi_{|S|}\) 开始不断跳 \(\pi\) 由长度从大到小遍历。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote class="border-l-4 border-neutral-300 dark:border-neutral-600 pl-4 italic text-neutral-600 dark:text-neutral-400 my-6"&gt;
&lt;p&gt;只需证明 \(S_{1 \sim \pi_{\pi_{n}}}\) 是 \(S\) 的次长 border 即可。&lt;/p&gt;
&lt;p&gt;由 \(\pi\) 的定义易知。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;相邻前缀函数满足 \(\pi_{i + 1} = \max\limits_{T \ is \ the \ border \ of \ S_{1 \sim i}, S_{i + 1} = S_{|T| + 1}} |T| + 1\)。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote class="border-l-4 border-neutral-300 dark:border-neutral-600 pl-4 italic text-neutral-600 dark:text-neutral-400 my-6"&gt;
&lt;p&gt;使用反证法易知。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="pi-函数的线性求法"&gt;pi 函数的线性求法&lt;/h3&gt;
&lt;p&gt;有了上述几条性质，我们可以得到一个求解 \(\pi\) 函数的算法：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-cpp" data-lang="cpp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;pi&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pi&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pi&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;pi&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;直观感受上这个做法是 \(\Theta(n^{2})\) 的，但实际上它是线性的，复杂度证明如下：&lt;/p&gt;
&lt;blockquote class="border-l-4 border-neutral-300 dark:border-neutral-600 pl-4 italic text-neutral-600 dark:text-neutral-400 my-6"&gt;
&lt;p&gt;不难发现复杂度来源在于跳 \(\pi\)，下面我们将证明跳 \(\pi\) 的总次数是线性的。&lt;/p&gt;
&lt;p&gt;注意 \(\pi_{i}\) 的位置变化情况，不难发现 \(\pi_{i}\) 只会每次 \(+1\) 或往前跳若干次，但往前跳的的总距离不能超过当前 \(+1\) 的次数，故总路程也是线性的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="pi-函数的基本应用"&gt;pi 函数的基本应用&lt;/h3&gt;
&lt;h4 id="字符串匹配"&gt;字符串匹配&lt;/h4&gt;
&lt;p&gt;求解模式串 \(T\) 在匹配串 \(S\) 中的出现位置的问题。&lt;/p&gt;
&lt;p&gt;构造一个新的字符串 \(T\circ S\)，其中 \(\circ\not\in S\cup T\)，它仅作为一个分隔符。&lt;/p&gt;
&lt;p&gt;对这个新的字符串求其 \(\pi\) 函数，不难发现由于分隔符的出现，\(\forall i, \pi_{i} \leqslant |T|\)，于是找到 \(\pi_{i} = |T|\) 的位置，\(i - |T| + 1 \sim i\) 就是一个匹配。&lt;/p&gt;
&lt;p&gt;以上即为 \(\rm KMP\) 算法。&lt;/p&gt;
&lt;h4 id="统计每个前缀的出现次数"&gt;统计每个前缀的出现次数&lt;/h4&gt;
&lt;p&gt;考虑到如果某个前缀在之后的位置里出现过，那么一定是某个前缀的 border，于是我们考虑在每个数的最大 border 处统计答案，其为更小 border 的重复次数，并且更小的 border 一定可以通过跳 \(\pi\) 遍历到，这样只需要从后往前扫一遍，复杂度 \(\Theta(n)\)。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-cpp" data-lang="cpp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ans&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ans&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pi&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;ans&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;至于这个问题的加强版，统计 \(S\) 中的每个前缀在 \(T\) 中的出现次数，可以构造 \(S\#T\)，赋初值的时候只需要将 \(T\) 中的位置初始化为 \(1\) 即可。&lt;/p&gt;
&lt;h4 id="统计本质不同子串的数目"&gt;统计本质不同子串的数目&lt;/h4&gt;
&lt;p&gt;考虑迭代计算，问题转化成：在当前字符串的末尾加入一个新的字符，以这个字符结尾的后缀中有多少个是前面没有出现过的。&lt;/p&gt;
&lt;p&gt;如果一个子串 \(S_{i\sim n}\) 在前面出现过，则 \(\forall j\geqslant i,S_{j\sim n}\) 都在前面出现过。&lt;/p&gt;
&lt;blockquote class="border-l-4 border-neutral-300 dark:border-neutral-600 pl-4 italic text-neutral-600 dark:text-neutral-400 my-6"&gt;
&lt;p&gt;由反证法易知。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;于是就只需求出加入新字符后的串的反串中的 \(\max\pi\) 即可，而这个字符的贡献就是 \(|S|+1-\max\pi\)，时间复杂度 \(\Theta(n^{2})\)。&lt;/p&gt;
&lt;h2 id="字符串周期相关性质"&gt;字符串周期相关性质&lt;/h2&gt;
&lt;p&gt;称 \(k\) 为 \(S\) 的一个周期且仅当 \(\forall i\leqslant |S|-k, S_{i} = S_{i + k}\)，特别地若 \(k \nmid S\) 则称 \(k\) 为 \(S\) 的弱周期。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;若字符串 \(S\) 存在一个 border \(T\)，当且仅当 \(S\) 存在一个长度为 \(|S| - |T|\) 的（弱）周期。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote class="border-l-4 border-neutral-300 dark:border-neutral-600 pl-4 italic text-neutral-600 dark:text-neutral-400 my-6"&gt;
&lt;p&gt;必要性显然，充分性递归论证即可。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;字符串 \(S\) 的最短（弱）周期长度为 \(n - \pi_{n}\)。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote class="border-l-4 border-neutral-300 dark:border-neutral-600 pl-4 italic text-neutral-600 dark:text-neutral-400 my-6"&gt;
&lt;p&gt;由 \(\pi\) 函数的定义易知。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;若长度不小于 \(p + q\) 的字符串 \(S\) 存在长度分别为 \(p, q\) 的（弱）周期，那么 \(\gcd(p, q)\) 也是 \(S\) 的一个（弱）周期。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote class="border-l-4 border-neutral-300 dark:border-neutral-600 pl-4 italic text-neutral-600 dark:text-neutral-400 my-6"&gt;
&lt;p&gt;下面首先证明 \(p - q(p \gt q)\) 也是 \(S\) 的一个（弱）周期。&lt;/p&gt;
&lt;p&gt;分两种情况讨论：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;若 \(i \leqslant q\)，则 \(S_{i} = S_{i + p} = S_{i + p - q}\)，可知 \(\forall i \leqslant q, p - q\) 是其一个（弱）周期。&lt;/li&gt;
&lt;li&gt;若 \(i \gt q\)，则 \(S_{i} = S_{i - q} = S_{i + p - q}\)，可知 \(\forall i \gt q, p - q\) 是其一个（弱）周期。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;证明 \(p - q\) 是 \(S\) 的一个（弱）周期后，根据更相减损术的性质，最终可以迭代至 \(\gcd(p, q)\) 是 \(S\) 的一个（弱）周期。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;所有强周期的长度均为最短强周期长度的倍数。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote class="border-l-4 border-neutral-300 dark:border-neutral-600 pl-4 italic text-neutral-600 dark:text-neutral-400 my-6"&gt;
&lt;p&gt;若存在一个强周期 \(x\)，显然有 \(x\mid n\)，若 \(n-\pi_{n}\nmid x\)，则 \(\gcd(n-\pi_{n},x)\lt n-\pi_{n}\) 也为一个强周期，与 \(n-\pi_{n}\) 是最小强周期矛盾。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;字符串 \(S\) 存在周期当且仅当 \(n - \pi_{n}\) 为一个周期。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote class="border-l-4 border-neutral-300 dark:border-neutral-600 pl-4 italic text-neutral-600 dark:text-neutral-400 my-6"&gt;
&lt;p&gt;必要性显然。若存在强周期 \(x\mid n\)，又因为 \(n-\pi_{n}\mid x\)，则 \(n-\pi_{n}\mid n\)，充分性得证。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="kmp-自动机"&gt;KMP 自动机&lt;/h2&gt;
&lt;p&gt;在求 \(\pi\) 函数的时候，我们发现这个做法是支持在线插入一个字符在末尾并计算函数值的。&lt;/p&gt;
&lt;p&gt;并且在做 \(\rm KMP\) 的过程中我们发现，在匹配串每一位求 \(\pi\) 函数时，并不关心之前匹配串的字符是什么，只关心之前的 \(\pi\) 函数值和当前位的字符。&lt;/p&gt;
&lt;p&gt;这意味着可以对一个模式串建立一个自动机，结点 \(i\) 表示当前节点的 \(\pi\) 值为 \(i\)，新加入一个节点的出边 \(c\) 指向跳 \(\pi\) 时第一个能够匹配的结点。暴力建这个自动机的复杂度为 \(\Theta(n^{2}|\Sigma|)\)，因为出边需要遍历 \(\Sigma\)，所以不存在求 \(\pi\) 函数时指针单方向移动的性质。&lt;/p&gt;
&lt;p&gt;但是实际上之前已经计算了从所有节点开始，以任意一个字符为出边会到哪个节点，故可以直接调用。具体规则为，记 \(f_{i,c}\) 表示从 \(i\) 号节点开始，字符 \(c\) 会到达的节点。如果当前到达了节点 \(i\) ，字符为 \(c\)，则如果 \(c=s_{i+1}\)，那么当前节点指向 \(i+1\)，否则指向 \(f_{\pi_{i},c}\)。时间复杂度 \(\Theta(n|\Sigma|)\)。&lt;/p&gt;
&lt;p&gt;同 \(\rm KMP\) 算法一样，\(\rm KMP\) 自动机也可以用来实现若干文本串和模式串的匹配，文本串从节点 \(0\) 开始走，每次走到 \(|S|\) 都完成了一次匹配。若模式串长度为 \(m\)，文本串长度为 \(n\)，数量为 \(t\)，则时间复杂度为 \(\Theta(m+tn|\Sigma|)\)。&lt;/p&gt;
&lt;p&gt;另一方面，\(\rm KMP\) 自动机在完成普通字符串匹配时表现并不优秀，但它可以求解一些特殊的字符串匹配问题，如：&lt;/p&gt;
&lt;p&gt;定义 \(g_{1}="a"\)，\(g_{2}="aba"\)，\(g_{3}="abacaba"\cdots \forall i,g_{i}=g_{i-1}+(char)i+g_{i-1}\)。 给定另一字符串 \(S\) 和整数 \(k\)，求 \(S\) 在 \(g_{k}\) 中的匹配次数。保证 \(|S|\times k\leqslant 10^{6}\)。&lt;/p&gt;
&lt;p&gt;显然题目是要求从 \(0\) 号节点开始对 \(g_{k}\) 进行匹配，经过 \(|S|\) 号节点的次数是多少。&lt;/p&gt;
&lt;p&gt;首先对 \(S\) 建立 \(\rm KMP\) 自动机，记 \(f_{i,j}\) 表示从 \(i\) 号节点开始对 \(g_{j}\) 进行匹配后经过 \(|S|\) 号节点的次数。记 \(k_{i,S}\) 表示从 \(i\) 号节点开始，匹配完字符串 \(S=g_{i}+(char)i\) 后停下来的结点，那么有 ：&lt;/p&gt;
\[
f_{i,j}=f_{i,j-1}+[k_{i,g_{j-1}+(char)(j-1)}=|S|]+f_{k_{i,g_{j-1}+(char)(j-1)},j-1}
\]&lt;p&gt;总时间复杂度 \(\Theta(|S|\times k)\)。&lt;/p&gt;
&lt;p&gt;另外，由于单次添加结点的复杂度是严格 \(\Theta(|\Sigma|)\) 的，所以可以对 \(\rm KMP\) 自动机进行可持久化处理，总时间复杂度 \(\Theta(n|\Sigma|\log n)\)。&lt;/p&gt;
&lt;p&gt;例题：
&lt;/p&gt;
&lt;p&gt;考虑对模式串建立 \(\rm KMP\) 自动机。&lt;/p&gt;
&lt;p&gt;令 \(f_{i,j}\) 表示对字符串匹配到第 \(i\) 位时，跳到自动机的 \(j\) 号点匹配的次数最大值，显然有转移：&lt;/p&gt;
\[
f_{i, to} =
\begin{cases}
\max\{f_{i-1, j} + [to = m]\} &amp; (s_i \not= '?') \\
\max\limits_{k = 0}^{25}\{f_{i-1, j} + [to = m]\} &amp; (s_i = '?')
\end{cases}
\]&lt;p&gt;由于 \(\rm DP\) 空间开不下，需要使用滚动数组。&lt;/p&gt;
&lt;p&gt;时间复杂度 \(\Theta(|S||T||\Sigma|)\)。&lt;/p&gt;</description></item></channel></rss>