前缀函数与 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\) 我们有

\[\delta(\pi_i,s_{i+1})= \begin{cases} \pi_{i+1}=j+1 & s_{i+1}=s_{j+1} \\ \pi_{i+1}=0 & s_{i+1}\neq s_{j+1}\land j=0 \\ \pi_{i+1}=\delta(\pi_j,s_{i+1}) & s[i+1]\neq s[j+1]\land j>0\end{cases} \]

  • 即前缀函数自动机,有时也被称为 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|)\)

posted @ 2023-02-06 11:20  未欣  阅读(61)  评论(0编辑  收藏  举报