前缀函数与 KMP
前缀函数
-
前缀函数 \(\pi_i\) 为 \(s_{1\dots i}\) 的最大相同真前后缀长度。
-
如无特别说明,本文中所有字符串下标从 \(1\) 开始,长度为 \(n\)。
-
考虑怎么求前缀函数。
-
首先肯定能想到暴力的 \(O(n^3)\) 枚举 \(i,len,j\) (子串长度,匹配长度,然后一位位验证)。但太蠢了,找一点性质。
-
结论一:\(\pi_{i+1}\leqslant \pi_i+1\) 。
- 证明:最多多配一位。如果多配了 \(k\) 位,说明
\[s_{\pi_{i+1}-k+1\dots \pi_{i+1}}=s_{i+1-k+1\dots i+1} \]- 从而
\[s_{\pi_{i+1}-k+1\dots \pi_{i+1}-1}=s_{i+1-k+1-1\dots i} \]- 即
\[s_{\pi_{i+1}-k+1\dots \pi_{i+1}-1}=s_{i-k+1\dots i} \]-
说人话就是,多配的这几位除了 \(s_{i+1}=s_{\pi_{i+1}-1}\) 这一位以外,其他几位在 \(i\) 处也可以配上,毕竟他们一样,而且都是 \(s_{1\dots i}\) 的子串。只有新增的这一位是 \(i+1\) 时才能配上的(后缀意义下是向右新走了一位)。
-
真前后缀意义下,\(\pi_{i+1}<i+1\),所以一定有 \(\pi_{i+1}-1<i\) 。
-
上面的优化可以抽象化为 \(\delta(\pi_i,s_{\pi_i+1})=\pi_i+1(s_{i+1}=s_{\pi_i+1})\) 。那么,能不能构造出相应的 \(s_{i+1}\neq s_{\pi_i+1}\) 的转移?
-
结论二:考虑在 \(s_{i+1}\neq s_{\pi_i+1}\) 时将尝试匹配的长度从 \(\pi_i+1\) 减小到 \(j+1\),使得对于 \(s_{1\dots i},s_{1\dots j}=s_{i-j+1\dots i}\)。运用后缀链接思想,\(j=\pi_i\)。
-
从而 \(\text{if } s_{i+1}=s_{j+1},\pi_{i+1}=j+1\),仍然失配则求 \(j'\)。可以看出这是一个递归过程,边界条件为 \(j=0\),此时仍然不行则 \(\pi_{i+1}=0\)。
-
所以 \(j'=\pi_j(j>0)\)。
-
-
综上所述,令 \(j=\pi_i\) 我们有
-
即前缀函数自动机,有时也被称为 KMP 自动机。
-
我们设势函数 \(\phi(i)=\pi_i\),下面的 \(\phi(\pi)\) 指的就是 \(\phi\),只是为了强调和当前的 \(\pi\) 强关联。
-
则转移 \((1)\) 的摊还代价等于 \(O(1)+\phi(1)\),转移 \((2)\) 的摊还代价等于 \(O(1)\)。
-
极限情况下 \(\pi_i=i\),即单次回跳仅仅让 \(\pi-1\)。从而转移 \((3)\) 的单次复杂度上界为 \(O(\pi_i)-\phi(\pi_i)+\phi(\pi_{i+1})\)。注意,负的势函数变化量才是正的时间复杂度。
-
实际上的操作过程中可能先 \((3)\) 再 \((1)/(2)\),但这可以忽略,总次数都是 \(O(n)\)。
-
显然任意次转移 \((1)\) 提供的复杂度和势函数是 \(n\) 以下的,于是因为转移 \((3)\) 每次回跳至少令势减小 \(1\),而总势不超过 \(n\),从而回跳次数不超过 \(n\)。
-
故此算法复杂度为 \(O(n)\)。该算法可以在线,即一位位地读。给出示范代码。
il void kmp(){
int j=0,n=s.size()-1;
For(i,2,n){
while(j && s[i]!=s[j+1]) j=pi[j];
j+=s[i]==s[j+1];
pi[i]=j;
}
return;
}
KMP 算法
求出现
-
考虑求 \(T\) 中 \(P\) 的所有出现位置。
-
原始的 KMP 算法如下:
-
构造字符串 \(P+x+T\),这里 \(x\) 是一个分隔符。需要保证其不在 \(P\) 或 \(T\) 中出现。
-
求该字符串的前缀函数。不妨记 \(|P|=n\),则对于所有 \(i>n+1\land \pi_i=n\) 的位置,\(T\) 中存在一个以 \(i\) 结尾的 \(P\)。之所以是等于号,是因为总有 \(\pi\leqslant n\)。
-
-
该算法其实不太聪明。目前比较常用的做法是,求出 \(P\) 的前缀函数,然后扫一遍 \(T\),维护当前在前缀函数自动机上的位置。复杂度可以通过同样的势能分析做到 \(O(n+m)\),这里 \(m=|T|\)。
-
考虑推广到特殊一点的情况,譬如势能分析不成立的情况。此时注意到我们的复杂度主要是因为连续失配而炸掉,考虑将失配边路径压缩,即将 \(\delta\) 边(正边)和 \(\pi\) 边(反边)结合起来直接构造 \(e(\pi,c)\),做到 \(O(1)\) 寻找后继。可以通过倍增实现或者类 AC 自动机实现(事实上,AC 自动机就是多模式串的前缀函数自动机,只是因为多模式串而套了个 trie 的壳子并略改变了 fail 边的逻辑),这里不赘述。
求前缀出现
-
考虑求 \(S\) 中所有前缀的出现次数。
-
我们知道这是 AC 自动机上 DP 的经典问题,但考虑一下能不能不建自动机解决它。
-
可以。首先将 \(\pi\) 的贡献计算(不考虑 \(\pi_\pi\)),然后类似完全背包,利用一下后效性,倒序枚举前缀的长度,\(t_{\pi_i}+=t_i\)。
-
最后将本来就是前缀的贡献加一下。
-
考虑求 \(T\) 中 \(S\) 所有前缀的出现次数。
-
没有本质区别,只是略去第三步。
求周期
-
考虑求 \(S\) 的最短周期。定义 \(S\) 的周期为 \(p\),使得 \(\forall i\in [1,n-p+1],S_i=S_{i+p}\)。
-
引入 border:若 \(s\) 是 \(S\) 的一个真前缀且 \(\pi_n\geqslant |s|\),则 \(s\) 为 \(S\) 的一个 border。显然 \(S\) 可以有很多不同长的 border,但我们一般只关心最长的。
-
有结论:\(S\) 的最短周期 \(p=n-|s|\)。由定义易得,毕竟这相当于直接把 border 平移到了和它匹配的后缀上。
求压缩
-
考虑求 \(S\) 的最短压缩。定义 \(S\) 的压缩为 \(|s|\),满足 \(s\) 是 \(S\) 的前缀且 \(S\) 可由一个或多个 \(s\) 拼接而来。
-
结论:若 \((n-\pi_n)\mid n\),则最短压缩 \(r=n-\pi_n\),否则最短压缩 \(r=n\)。
-
不妨将 \(S\) 划分为若干段,每段长都为 \(r\)。于是长为 \(n-r=n-(n-\pi_n)=\pi_n\) 的前后缀应当相等。
-
但这代表着第一块和第二块相等,同理第二块和第三块相等,etc.
-
其最优性显然,若存在更短压缩,则会有更短块,于是 \(r-1\) 块会更长,故 \(\pi_n\) 会更长。
-
若不可整除,则显然即使存在 \(r\),也会有 \(r>n-\pi_n\)(证明同最优性证明),此时对 \(\pi_n\) 分讨即可:
-
若 \(\pi_n<=\frac{n}{2}\),则 \(r>n-\pi_n>=\frac{n}{2}\),显然想要整除只能为 \(n\)。
-
否则,我不会证。
-
-
在线求本质不同的子串数目
-
考虑求 \(S\) 中本质不同的子串数目,要求支持双端加/删字符。
-
都在线了,肯定是递推啊...不然叫 SA 来不就好了(当然 \(O(n)\) SA 科技可以硬吃在线,可惜我不会)。
-
只讨论后端加字符。构造 \(Sn=S+c\),将其翻转获得 \(Sn'\),对 \(Sn'\) 求前缀函数,则长度不大于 \(\max \pi\) 的所有前缀都在 \(S\) 中出现过,故新增子串数为 \(|S|+1-\max \pi\)。
-
其他情况显然同理,复杂度为 \(O(n^2)\)。
如果是初始+操作的话,初始也许可以考虑用 SA 来卡常。
CF1200E Compress Words
- 题意略。不断求第二个串的 \(\pi\),然后和第一个串的后缀匹配,总复杂度 \(O(\sum |S|)\)。
CF808G Anthem of Berland
-
题意略。
-
建出 \(S\) 的前缀函数自动机,设计 dp 如下:
-
状态设计:\(f_{i,j}\) 表示当前考虑完 \(T\) 的前 \(i\) 位,当前匹配了 \(j\) 位,最大已有匹配次数。
-
初始化:\(f_{0,0}=0\)。
-
状态转移:读字符或枚举字符,然后暴力转。
-
-
复杂度 \(O(|S||T||\Sigma|)\)。
P2375 [NOI2014] 动物园
-
题意略。
-
首先显然可以建出前缀函数自动机来解决,但一个更妙的方式是,用求 \(\pi\) 的方式来求 \(num\),因为对 \(num\) 来说两个结论都成立。于是 \(O(T|S|)\)。