2022.9.22 闲话

TMD 做 CF1C 一直 WA13,3 机房上不去 CF 是啥玩意???我为啥不能给 Eafoo 的博客评论???难道我被认知危害了吗????

我打算放 メリーバッドエンド (Merry Bad End) 歌词,现在假装下面有歌词:

メリーバッドエンド

Merry Bad End

KMP 自动机相关的一些杂谈

提到 KMP,给大家提供一道水题:Ynoi2011 遥远的过去(有点码农,我可能要鸽挺久了 TAT

KMP 自动机

以前一直以为 KMP 算法本来就是一个天然的自动机,直到看到 CF1721E 才发现原生 KMP 没有 fail 指针 QwQ .

现在来看一下真正的 KMP 自动机 .

从前缀 border 开始

KMP 的一个开端也是前缀的 border,一些关于 border 的东西可以看 Border Theory,理解不深刻轻喷(实际上目前 IPM 还没有几个 OIer 实现出来).

约定:首先后面的东西都是对于一个原串 \(S\) 来说的,不妨令其字符集为 \(\Sigma\),其他约定见前面的 Border Theory .

定义前缀函数(也就是 next 数组)为 \(\pi_i\) 表示 \(S[1:i]\) 的最长 border 长度 .

KMP 算法指出了一个 \(O(|S|)\) 计算 next 数组的方法,不是很容易用语言简单表述就挂个链接跑路吧 XD:KMP - OI Wiki .

注意到它对于每一位的计算是 均摊 \(O(1)\) 的,这就表明它不适用于动态问题 .

Border 树

对于每个点 \(i\),连边 \(i\to\pi_i\),于是前面的过程就是在 Border 树上跳 .

具体的原因可以看 Border Theory,事实上是非常自然的 .

于是我们可以自然的引入 fail 指针(怎么都是自然的):

fail 指针

为了让 KMP 更像自动机一点,我们先让每个点可以向任意一个字符转移,定义广义前缀函数 \(\pi_{i,c}\) 表示表示从 \(i\) 开始沿着 Border 树向上跳得到的最大的位置 \(k\text{ s.t. }s_{k+1}=c\) .

然后引入 fail 指针 \(f_i=\pi_{f_{i-1},S_i}\) .

这样我们的 \(\pi\) 就可以单次严格 \(O(|\Sigma|)\) 递推了:

  • \(S_{i+1}=c\),则 \(\pi_{i,c}=i\) .
  • \(S_{i+1}\neq c\),则 \(\pi_{i,c}=\pi_{f_i,c}\) .

看起来就是 AC 自动机转移啊?事实上 KMP 自动机也就是单串的 AC 自动机了 .

例题 / Prefix Function Queries

Prefix Function Queries

给一个字符串 \(S\)\(q\) 组询问,每次给一个字符串 \(T\),求 \(S+T\) 的后 \(T\) 位的前缀函数值 .

\(1\le |S|\le 10^6\)\(1\le q\le 10^5\)\(1\le |t|\le 10\),字符集为小写字母 .

每次加一个字符只需要改一位 next 一位 fail,然后就随便做了 .

时间复杂度 \(O(|\Sigma|(|S|+\sum|t|))\) .

例题 / Anthem of Berland

Anthem of Berland

给两个串串 \(s,t\)\(s\) 中可能有问号 .

把每个问号换成一个小写字母,问 \(t\) 匹配 \(s\) 的次数最大是多少 .

\(1\leq |s|,|t|\leq 10^5\)\(|s|\cdot |t|\leq 10^7\) .

除了问号字符集是小写字母 .

KMP 自动机上 DP .

\(dp_{i,j}\) 表示 \(s[1:i]\) 走到 KMP 自动机上的节点 \(j\) 时匹配的最大次数,转移显然 .

时间复杂度 \(O(|s|\cdot |t|\cdot |\Sigma|)\) .

其实可以类比 AC 自动机上 DP 的啦 .

可持久化 KMP

好像是叫这个名字 /yun .

首先放一下题面,免得大家不知道我在说什么:

Sequential Prefix Function

维护一个字符串 \(S\)(初始为空),\(n\) 次操作:

  • 在末尾添加一个字符 .
  • 删掉末尾的字符 .

\(1\le n\le 2\times 10^5\) .

下面提供一个 KMP 自动机做法 .

首先建 KMP 自动机,点数记作 \(m\),根记作 \(r\) .

对于一个点,我们需要求的就是既是 Border 树上的祖先又是 fail 树上的祖先中深度最大的那个,先预处理出 Border 树的 DFS 序,\(u\) 点的 DFS 序记作 \(\operatorname{dfn}(u)\),然后还要顺便记一下每个点的子树 DFS 序的 max,记作 \(\operatorname{maxdfn}(u)\) .

在 fail 树上 DFS,走到一个点 \(u\) 的时候,如果 \(u\leftrightarrow r\) 的路径上存在一个点 \(v\) 使得 \(\operatorname{dfn}(v)\in[\operatorname{dfn}(u),\operatorname{maxdfn}(u)]\) 的时候,\(v\) 就可能作为答案,于是建一棵线段树,每个点存一个链表,每次访问点 \(u\) 的时候对于 \([\operatorname{dfn}(u),\operatorname{maxdfn}(u)]\) 上的每个点后面都链一个 \(u\),走出去的时候就删掉,于是查询就只需要查询线段树中到 \(\operatorname{dfn}(u)\) 的路径中深度最大的点,这个不难维护 .

时间复杂度大概是 \(\Theta(n\log n)\) .

还有一些其他做法可以做到 \(\Theta(n)\),这里就不说了 .

posted @ 2022-09-22 20:26  Jijidawang  阅读(89)  评论(1编辑  收藏  举报
😅​