编程语言
首页 > 编程语言> > NOI 算法梳理

NOI 算法梳理

作者:互联网

距离国赛只剩 15 days 了,而 tzc_wk 甚至在上周四的杭电多校中被 polya 定理板子卡了好久,原因竟然是忘了 polya 定理的板子怎么写了,这不是菜是什么,所以,趁着时间还算有点充足,好好复习下 NOI 要考的知识点吧(

下文已将知识点按照大模块分类,由于时间不够了某些地方可能会写得很简略,尽量更,如果实在更不完也没关系。这一周大概不会刷新题了,大概会把每类知识点的解题方法梳理一下(

1. 字符串方向(考察概率:40%)

1.1 KMP

KMP 算法一般用于解决以下两类问题:

求解前者的方法就是维护一个指针 \(j\),初始为 \(kmp_{i-1}\),然后不断跳 \(kmp\) 直到 \(s_{j+1}=s_i\) 为止。求解后者的方法也类似,每插入一个字符就跳 \(fail\) 直到当前指针的下一个位置与待插入字符相同。由于每次插入字符最多使指针在 KMP 树上的深度加 1,所以复杂度线性。

1.2 Z 算法

Z 算法可以做以下事情:

求解方法类似于暴力,但是与暴力不同的地方在于,假设我们已经求完了 \(z_2\sim z_i\),我们实时维护使 \(z_j+j\) 取到最大值的 \(j\),设其为 \(x\),那么我们求一个 \(z_i\) 时,如果 \(i<z_x+x\),我们就以 \(\min(z_{i-x+1},z_x+x-i)\) 为 \(z_i\) 的初始值。不难发现,\(z_x+x\) 是单调递增的,因此该算法复杂度也是 \(\mathcal O(|s|)\) 的。

1.3 AC 自动机

可以看作 KMP 的扩展,只不过复杂度需要乘一个 \(\Sigma\)。

如果模式串只有一个,那 KMP 显然是没有问题的,但是如果模式串有多个直接 KMP 显然复杂度多个 \(n·|t|\)。考虑建出这些模式串的 trie 树,对于 trie 树上的每个节点 \(x\) 定义 \(fail_x\) 表示 trie 树上深度最深的节点 \(y\) 满足 \(root\to y\) 组成的字符串是 \(root\to x\) 的一个后缀。求解 fail 的过程就从根节点开始 BFS,类比 KMP,求解 \(fail_x\) 就从 \(fa_x\) 开始不断跳 fail 直到存在一个等于 \(x\) 与 \(fa_x\) 之间的字符的出边,移到对应节点即可。

事实上关于这个 \(fail\) 还有更深层次的理解,考虑文本串与这一堆模式串匹配的过程,那么在任意时刻肯定存在一个最深的点 \(x\) 使得当前字符串的后 \(dep_x\) 个节点刚好对应节点 \(x\),显然在匹配的时候我们不用关心 \(dep_x\) 个字符往前的内容,因为它不可能走到一个模式串的位置,因此我们就设 \(x\) 为文本串匹配的状态。那么加入一个字符 \(c\) 的时我们会转移到一个新的状态 \(nw\),那么如果 \(x\) 存在 \(c\) 的出边,显然 \(nw\) 就是对应的儿子,否则我们就要退一步,但是为了避免错过可能的状态,我们只能退到 trie 树上能表示出来的最深的点,也就是 \(fail_x\),如此跳下去直到能接上字符 \(c\) 为止。

AC 自动机是离线算法,即,不能支持在某个文本串后面加入字符后动态维护 \(fail\) 的变化,如果碰到类似的题需要时间轴分块/二进制分组。

将 \(x\to fail_x\) 连边,会得到一棵树称为 fail 树,那么求模式串在给定文本串的总出现次数,等价于对于文本串每个前缀,在 AC 自动机 trie 图上定位到其位置,然后将所以模式串的终止节点标记为关键节点,统计 fail 树上该节点到根路径上关键节点个数求和,因此 AC 自动机也常与树论结合。

1.4 SA

考虑倍增:我们考虑维护一个长度 \(len\),初始 \(len=1\),然后每一轮令 \(len\) 乘 \(2\) 并通过形如 \(s[i...i+len-1]\) 的 \(n\) 个子串排序后的结果来得到形如 \(s[i...i+2len-1]\) 排序后的结果。具体方法就是,设 \(rk_i\) 表示 \(s[i...i+len-1]\) 的排名,那么等价于将形如 \((rk_i,rk_{i+len})\) 当作二元组排序,由于值域只有 \(\mathcal O(n)\),排序可以桶排。注意相同的二元组的排名也应相同,其他方面在实现上还有一些注意点,譬如二元组的第二维其实并不需要桶排,可以直接 \(O(n)\) 地扫一遍得到其大小关系,然后再对第一维桶排即可,时间复杂度线性对数。

后缀数组可以延申出一套定理,在下面的讨论中我们设 \(sa_i\) 表示排名为 \(i\) 的后缀是谁,\(rk_i\) 表示 \([i...n]\) 的排名,那么我们假设 \(ht_i=\text{LCP}(s[sa_i...n],s[sa_{i+1}...n])\),那么对于排名 \(x,y\) 的后缀 \(sa_x,sa_y(x<y)\),有定理:\(\text{LCP}(s[sa_x...n],s[sa_y...n])=\min\limits_{i=x}^{y-1}ht_i\),证明略。这是一个 RMinQ 的形式,因此 SA 常与 DS 结合。那么如何求 \(ht_i\) 呢?令 \(h_i=ht_{rk_i}\),那么有定义 \(h_i\ge h_{i-1}-1\),因此从下标 \(1\) 枚举到 \(n\) 顺着扫一遍就好了。

这也就是为什么 SA 题目一般 getsa, getht, buildst, queryst 一遍写过去(

1.5 Manacher

Manacher 算法一般用于求解一个字符串最长回文子串长度。

考虑先对字符串做一个变换:对于字符串 \(s_1s_2\cdots s_n\) 我们构造 \(t=|s_1|s_2|\cdots|s_n|\),即在相邻两个字符之间加入分隔符。这样可以避免对字符串长度的奇偶性进行分类讨论。

考虑设 \(len_i\) 表示以 \(i\) 为中心的回文串半径的最大值,那么不难发现一个字符串最长回文子串的长度就是 \(\max\limits_{i=1}^{|t|}len_i-1\)。接下来考虑如何求 \(len\) 数组。类比 Z 算法,我们从左到右求这些 \(len\),定义一个位置的回文串 box 为 \([i-len_i+1,i+len_i-1]\),那么我们实时维护右端点最大的回文串 box,然后求解 \(len_i\) 的时候,假设右端点取到最大值的 box 为 \([x-len_x+1,x+len_x-1]\),那么如果 \(i\in[x-len_x+1,x+len_x-1]\),我们就令 \(len_i\) 的初始值为 \(\min(len_{2x-i},x+len_x-i)\),然后开始扩展即可,这样每多匹配一格右端点就会加一,复杂度线性。

推论:一个字符串本质不同回文子串个数是线性的,因此碰到回文串有关问题可以考虑这些性质。

1.6 SAM

1.6.1 SAM 与后缀树

由于学 SAM 时比较咕没写学习笔记所以现在甚至不太记得 SAM 怎么写了(捂脸,毕竟最近 NOI 模拟赛也没考过 SAM)

对于给定字符串 \(s\),SAM 是一个能够识别其所有子串的自动机。更具体地,从初始状态到所有状态的路径都是 \(s\) 的一个子串,并且 \(s\) 的所有子串都可以通过初始状态到某个状态的某条路径表示出来。显然 SAM 有 \(n^2\) 的建法,太逊了,考虑如何 \(O(n)\) 地建 SAM。先抛出一些定义:

有了这些定义以后我们可以直观地想到:将所有 endpos 相同的状态缩成一个等价类,那么感性地理解这些等价类个数不会太多,因此我们考虑将每个等价类看作一个状态。在进行接下来的讨论之前先抛出一些引理:

  1. 任意两字符串的 endpos 要么包含要么不交,读者自证不难。

  2. endpos 相同的字符串的长度构成一段连续的区间,且较短者永远是较长者的后缀,读者自证不难。

  3. 状态数的上界是 \(2n-1\),读者自证不难(显然将区间的包含关系连边会连出一棵树,其叶子节点上界是 \(n\))


引理三为我们将后缀自动机复杂度降到线性埋下了基础。接下来引入另一个定义:

定义一个字符串的后缀树为 \(i\to link(i)\) 连边后形成的树(为什么是树呢?因为显然在一个字符串开头砍掉若干个字符后它的 \(endpos\) 集合大小肯定是单调不降的,根据引理 \(1\),这张图必然不会成环,因此它是树)

当然,由 link 的定义也可以直接得出一些结论:


上面的讨论是基于后缀树的一些理论,那么我们又该如何将这套理论与自动机的理论结合在一起呢?由于我们将 edp 相同的缩成了一类,所以有关状态和转移的定义也需做相应的修改:

这样我们可以知道,对于一个字符串表示的状态,在其末尾插入字符可以视作在自动机上转移,在其开头插入字符可以视作在后缀树上向其儿子转移。

1.6.2 SAM 的建立

现在我们已经知道了后缀树与后缀自动机的联系,下面我们要知道如何构建后缀自动机。

假设现在我们已经知道了当前字符串 \(s[1...n]\) 的后缀自动机,我们要在后面 append 某个字符 \(c\),考虑其变化。

首先我们显然可以在每插入一个字符的时候就维护整个串表示的状态 \(x\),首先我们添加转移 \(\delta(x,c)\),并令 \(y\) 为转移到的新节点。考虑加入这个节点会多出哪些变化。显然存在一个最长的后缀 \(s[i...n]\) 满足 \(s[i...n]+c\) 还是 \(s[1...n]\) 的一个子串,对于比这个后缀更长的后缀表示的状态 \(t\),我们添加转移 \(\delta(t,c)=y\),这一部分暴力跳即可。特判掉不存在这样的后缀的情况,此时直接令 \(link(y)\) 为根节点并返回。我们假设 \(s[i...n]\) 表示的状态为 \(p\),\(\delta(p,c)\) 表示的状态为 \(q\)。

显然,根据 edp 的定义,\(maxlen(q)\ge maxlen(p)+1\),此时我们分两种情况讨论:

时间复杂度可以被证明是线性的。但是限于篇幅原因,这里不证明。

1.6.3 一些后缀自动机的基本技巧

至于 PAM 什么的感觉 NOI 考的概率 \(<\epsilon\) 所以也不准备复习了,毕竟早忘了(

1.7 总结

对于 NOI 级别的字符串题,首先先需要明确它需要用什么知识点,一般来说如下:

1.8 好题整理

先咕着,有时间再进行这项操作。

标签:...,NOI,状态,后缀,len,算法,link,字符串,梳理
来源: https://www.cnblogs.com/ET2006/p/tzc_will_Au_NOI2022.html