前缀函数与 KMP

前缀函数

  • 前缀函数 πis1i 的最大相同真前后缀长度。

  • 如无特别说明,本文中所有字符串下标从 1 开始,长度为 n

  • 考虑怎么求前缀函数。

  • 首先肯定能想到暴力的 O(n3) 枚举 i,len,j (子串长度,匹配长度,然后一位位验证)。但太蠢了,找一点性质。

  • 结论一:πi+1πi+1

    • 证明:最多多配一位。如果多配了 k 位,说明

    sπi+1k+1πi+1=si+1k+1i+1

    • 从而

    sπi+1k+1πi+11=si+1k+11i

    sπi+1k+1πi+11=sik+1i

    • 说人话就是,多配的这几位除了 si+1=sπi+11 这一位以外,其他几位在 i 处也可以配上,毕竟他们一样,而且都是 s1i 的子串。只有新增的这一位是 i+1 时才能配上的(后缀意义下是向右新走了一位)。

    • 真前后缀意义下,πi+1<i+1,所以一定有 πi+11<i

  • 上面的优化可以抽象化为 δ(πi,sπi+1)=πi+1(si+1=sπi+1) 。那么,能不能构造出相应的 si+1sπi+1 的转移?

  • 结论二:考虑在 si+1sπi+1 时将尝试匹配的长度从 πi+1 减小到 j+1,使得对于 s1i,s1j=sij+1i。运用后缀链接思想,j=πi

    • 从而 if si+1=sj+1,πi+1=j+1,仍然失配则求 j。可以看出这是一个递归过程,边界条件为 j=0,此时仍然不行则 πi+1=0

    • 所以 j=πj(j>0)

  • 综上所述,令 j=πi 我们有

δ(πi,si+1)={πi+1=j+1si+1=sj+1πi+1=0si+1sj+1j=0πi+1=δ(πj,si+1)s[i+1]s[j+1]j>0

  • 即前缀函数自动机,有时也被称为 KMP 自动机。

  • 我们设势函数 ϕ(i)=πi,下面的 ϕ(π) 指的就是 ϕ,只是为了强调和当前的 π 强关联。

  • 则转移 (1) 的摊还代价等于 O(1)+ϕ(1),转移 (2) 的摊还代价等于 O(1)

  • 极限情况下 πi=i,即单次回跳仅仅让 π1。从而转移 (3) 的单次复杂度上界为 O(πi)ϕ(πi)+ϕ(π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 算法

求出现

  • 考虑求 TP 的所有出现位置。

  • 原始的 KMP 算法如下:

    • 构造字符串 P+x+T,这里 x 是一个分隔符。需要保证其不在 PT 中出现。

    • 求该字符串的前缀函数。不妨记 |P|=n,则对于所有 i>n+1πi=n 的位置,T 中存在一个以 i 结尾的 P。之所以是等于号,是因为总有 πn

  • 该算法其实不太聪明。目前比较常用的做法是,求出 P 的前缀函数,然后扫一遍 T,维护当前在前缀函数自动机上的位置。复杂度可以通过同样的势能分析做到 O(n+m),这里 m=|T|

  • 考虑推广到特殊一点的情况,譬如势能分析不成立的情况。此时注意到我们的复杂度主要是因为连续失配而炸掉,考虑将失配边路径压缩,即将 δ 边(正边)和 π 边(反边)结合起来直接构造 e(π,c),做到 O(1) 寻找后继。可以通过倍增实现或者类 AC 自动机实现(事实上,AC 自动机就是多模式串的前缀函数自动机,只是因为多模式串而套了个 trie 的壳子并略改变了 fail 边的逻辑),这里不赘述。

求前缀出现

  • 考虑求 S 中所有前缀的出现次数。

  • 我们知道这是 AC 自动机上 DP 的经典问题,但考虑一下能不能不建自动机解决它。

  • 可以。首先将 π 的贡献计算(不考虑 ππ),然后类似完全背包,利用一下后效性,倒序枚举前缀的长度,tπi+=ti

  • 最后将本来就是前缀的贡献加一下。

  • 考虑求 TS 所有前缀的出现次数。

  • 没有本质区别,只是略去第三步。

求周期

  • 考虑求 S 的最短周期。定义 S 的周期为 p,使得 i[1,np+1],Si=Si+p

  • 引入 border:若 sS 的一个真前缀且 πn|s|,则 sS 的一个 border。显然 S 可以有很多不同长的 border,但我们一般只关心最长的。

  • 有结论:S 的最短周期 p=n|s|。由定义易得,毕竟这相当于直接把 border 平移到了和它匹配的后缀上。

求压缩

  • 考虑求 S 的最短压缩。定义 S 的压缩为 |s|,满足 sS 的前缀且 S 可由一个或多个 s 拼接而来。

  • 结论:若 (nπn)n,则最短压缩 r=nπn,否则最短压缩 r=n

    • 不妨将 S 划分为若干段,每段长都为 r。于是长为 nr=n(nπn)=πn 的前后缀应当相等。

    • 但这代表着第一块和第二块相等,同理第二块和第三块相等,etc.

    • 其最优性显然,若存在更短压缩,则会有更短块,于是 r1 块会更长,故 πn 会更长。

    • 若不可整除,则显然即使存在 r,也会有 r>nπn(证明同最优性证明),此时对 πn 分讨即可:

      • πn<=n2,则 r>nπn>=n2,显然想要整除只能为 n

      • 否则,我不会证。

在线求本质不同的子串数目

  • 考虑求 S 中本质不同的子串数目,要求支持双端加/删字符。

  • 都在线了,肯定是递推啊...不然叫 SA 来不就好了(当然 O(n) SA 科技可以硬吃在线,可惜我不会)。

  • 只讨论后端加字符。构造 Sn=S+c,将其翻转获得 Sn,对 Sn 求前缀函数,则长度不大于 maxπ 的所有前缀都在 S 中出现过,故新增子串数为 |S|+1maxπ

  • 其他情况显然同理,复杂度为 O(n2)如果是初始+操作的话,初始也许可以考虑用 SA 来卡常。

CF1200E Compress Words

  • 题意略。不断求第二个串的 π,然后和第一个串的后缀匹配,总复杂度 O(|S|)

CF808G Anthem of Berland

  • 题意略。

  • 建出 S 的前缀函数自动机,设计 dp 如下:

    • 状态设计:fi,j 表示当前考虑完 T 的前 i 位,当前匹配了 j 位,最大已有匹配次数。

    • 初始化:f0,0=0

    • 状态转移:读字符或枚举字符,然后暴力转。

  • 复杂度 O(|S||T||Σ|)

P2375 [NOI2014] 动物园

  • 题意略。

  • 首先显然可以建出前缀函数自动机来解决,但一个更妙的方式是,用求 π 的方式来求 num,因为对 num 来说两个结论都成立。于是 O(T|S|)

posted @   未欣  阅读(75)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示