2024集训D12总结
集训D12总结
知识点总结
字符串的知识点很庞杂 , 很多时候同一道题也有多种做法 .
也正是因为这个原因 , 自己在字符串方面算法的覆盖率恐怕是数学之外 (数学太庞大了) 最低的 . SAM , ACAM 这些最近也不经常写 . 正好借此机会把字符串重头过一遍 . 因此今天是知识点总结 .
大部分算法思路来源于自动机 和均摊复用两类 .
哈希
最常用的字符串算法 , 配合二分做到 \(\log\) 复杂度的 \(LCP\) . 同时哈希不局限在字符串领域 . 见[学习笔记]更全面的哈希总结 - youlv - 博客园 .
KMP
算法过程类似于构造自动机 . 不同的是因为固定在了一个序列上 , 因此没有显式地构造出转移 , nxt 数组相当于 fail 指针 , 单次构造过程就是在跳 fail . 因此 \(border\) 实质上构成了一颗树 .
border theory
引理1 : 字符串 \(|S|=n\) , 有长为 \(p\) 的 \(border\) , 等价于有 \(n-p\) 的周期 .
引理2 (弱周期引理) : 若 \(p,q\) 是 \(S\) 的周期 , 且 \(p+q\le n\) , 则 \(\gcd(p,q)\) 也是 \(S\) 的周期 .
引理3 (周期引理) : 若 \(p,q\) 是 \(S\) 的周期 , 且 \(p+q+\gcd(p,q) \le n\) , 则 \(\gcd(p,q)\) 也是 \(S\) 的周期 .
引理4 : 若 \(S\) 有周期 \(a \ (a\le n)\) , 其一个前缀有整周期 \(b\) , 且 \(b|a\) , 则 \(S\) 有周期 \(b\) .
引理5 : 若 \(S\) 在 \(T\) 上匹配位置间距 \(d,d\le n\) , 则 \(S\) 有长为 \(d\) 的周期 .
引理6 : \(S\) 的长度 \(\ge \frac{n}{2}\) 的 \(border\) 构成等差数列 .
引理7 : \(S\) 的所有 \(border\) 可以划分成 \(O(\log n)\) 个等差数列 . ( 划分方式是 \([2^{i-1},2^i]\cdots\) ) .
引理8 : \(S\) 的公差 \(\ge d\) 的 \(border\) 等差序列总大小是 \(O(n/d)\) 的 .
所谓 \(border\) 理论类似于一个结论集合 , 把一些偏直觉 , 同时在思维流程中常见的结论 . 进行了汇总 , 严谨化 .
KMP自动机
在 \(KMP\) 基础上 , 显式地对每个状态构造大小为 \(|\Sigma|\) 的转移 , 表示后加字符 \(c\) 后的转移 .
可以做到对一个串 \(S\) 构造一个 "通用的" 用于匹配的自动机 .
AC自动机
在\(Trie\) 树基础上 , 类似 \(KMP\) 自动机地构造转移和 fail . 这也是为什么 \(AC\) 自动机被称作 \(Trie\) 和 \(KMP\) 思想的结合 .
最基础的应用是对多模式串构造用于匹配的自动机 . 常见的拓展有在节点上 DP , 以及用数据结构维护 fail 树 .
manacher
用于求回文串的算法 , 核心是利用回文的对称性 , 复用了之前计算过的信息 , 而且通过最大右端点 \(r\) 只右移均摊保证了复杂度 .
Z Algorithm
感觉内涵思想和 \(KMP\) 关系不大 , 就不叫做 \(exKMP\) 了 . 反而可以从 manacher 举一反三 .
考虑计算到第 \(i\) 位 , 称某个点出发的 \([x,x+z_x-1]\) 为一组 \([zl,zr]\) , 记录 \(zr\) 最大的 \([zl,zr]\) , 发现类似于manacher , 也可以用 \([zl,zr]\) 的重复性复用一些信息 , 具体地 , \(i\) 对应位置是 \(j=i-zl+1\) , 最多能"搬运"过来的长度是 \(zr-i+1\) , 因此可以对 \(i\) 取 \(z_i=min(z_j,zr-i+1)\) , 并暴力拓展 , 因为拓展的位置一定 \(>zr\) , 均摊保证复杂度 .
意义是比较高效 , 简单地做整串均摊 \(O(n)\)的 \(LCP\) .
SA
相当巧妙的一种构造 , 把子串问题简洁地用一组序列表示了 .
\(sa_i\) 表示排名为 \(i\) 的后缀 , \(rk_i\) 表示后缀 \([i,n]\) 的排名 .
求后缀数组 , 常用两种方法 :
- $O(n\log n) $的倍增 , 注意几个细节和常数优化 :
- 维护关键字值域 \(W\) , 每次只存 \(W\) 范围内部分 , 初始大小为字符集 . 优化常数 .
- 反向维护第二关键字 . 用 \(y_i\) 表示第二关键字排名为 \(i\) (非严格) 的后缀 , 从大向下取 \(y_i\) , 填入对应的数就可以保证第二关键字 .
- 当值域 \(W=n\) 时排序完成 , 结束倍增 .
- $O(n \log ^2 n) $ , 直接用
std::sort
, 用二分哈希做check . 缺点是复杂度不优 , 常数也不小 . 优点是真的太好写了 , 错误空间也小 . 当然平时不能贪图好写 , 可是考场上或许有用处 .
用 \(sa\) 只能进行后缀比较的操作 , 更重要的是 \(height\) 这一概念 :
定义 \(height_i=LCP(sa_i,sa_{i-1})\) , 有引理1: \(height_{rk[i]}\ge height_{rk[i-1]}-1\) . 这保证了可以暴力处理 \(height\) 数组 , 而有均摊保证 \(O(n)\) 复杂度 .
而 \(height\) 的应用需要引理2: $$LCP(sa_i,sa_j)=\min \limits_{k=i+1}^j height_k$$ , 这将任意两个后缀的 $$LCP$$ 转化成了一个 \(RMQ\) 问题 . 而又有子串相当于某个后缀的前缀 , 因此 SA 成为了处理子串问题的利器 .
SAM
常常有人说 SA 和 SAM 是有互为替代的能力的 . 然而两者的思维相差很大 .
SAM 处理子串问题的基础是子串相当于某个前缀的后缀 , 维护 \(endpos\) 等价类 , \(fail\) 树与转移关系 . 可以说 , SAM 高度压缩地储存了所有子串和它们之间的后缀关系 .
应用时重点把握这些性质 :
-
\(fail\) 树就是反串的后缀树 . 所有节点维护的 \(endpos\)就是所有儿子 \(endpos\) 的加和 , 整个子树的并集 .
因此 , 任意前缀\(x,y\) 的 \(LCS\) 就是 \(len[LCA(x,y)]\) .
同时 , 可以用线段树合并在 \(fail\) 上维护 \(endpos\) 集合 .
-
在 SAM 上任意跑转移就是任意子串 .
-
节点在 fail 树上的拓扑序之一是按 \(len\) 排序 .
一般题目常常转化到 \(fail\) 树 dp , 这时可以直接按 \(len\) 桶排 , 在拓扑序上dp , 常数会减小到接受 \(O(n)\) 的程度 .
广义 SAM
以 \(Trie\) 为基础 , bfs 插入 \(SAM\) 构造出的自动机 . 用于同时维护多个串的子串 .
基本子串结构
众所周知正向 SAM \(\to\) 反向后缀树 , 反向 SAM \(\to\) 正向后缀树 , 而为了同时维护前后缀信息 , 是否可以实现魔改 SAM 达到更强大的维护能力 .
这个留待研究 . 因为 NOI 不能考 10 级的 SAM , 但是可以考 9 级的后缀树 . 如果同时考后缀树和 SAM 概率有点低 ( .
总结
总而言之 , 字符串部分的算法熟练度确实不太足啊 . 尤其是 SA , 之前几乎全在拿 SAM 做了 .
感觉对字符串没有足够深刻的体会的感觉 , 确实需要多练了 .