专题:字符串
KMP
几个定义:
- \(pre_i = s[1, i]\),\(suf_i = s[n - i + 1, n]\)。
- \(p\) 是 \(s\) 的 border 当且仅当 \(pre_p = suf_p\),特殊的,\(n\) 一定是 \(s\) 的 border。
- \(p\) 是 \(s\) 的 周期 当且仅当 \(\forall i > p,\ s_i = s_{i - p}\),特殊的,\(n\) 一定是 \(s\) 的 周期。
- 周期 \(p\) 是 \(s\) 的 循环节 当且仅当 \(p \mid n\),特殊的,\(n\) 一定是 \(s\) 的 循环节。
定理1:\(p\) 是 \(s\) 的周期等价于 \(n - p\) 是 \(s\) 的 border。
证明:\(q\) 是 \(s\) 的 border:\(s_1 = s_{n - q + 1}, \cdots, s_q = s_n\)。不难得到 \(n - q\) 是 \(s\) 的周期。
注意:border 没有二分性。
border 的暴力求法:枚举 \(1 \le i \le n\),检验是否有 \(pre_i = suf_i\),时间复杂度 \(O(n^2)\)。
优雅的暴力:哈希 \(O(1)\) 判定前后缀相等。缺点:常数大,会被卡。
定理2:\(s\) 的 border 的 border 也是 \(s\) 的 border。
证明:画个图很好理解。
推论:求 \(s\) 的所有 border 等价于求所有前缀的最大 border。
\(p = n\) 是 \(s\) 的最大 border,然后我们要求 \(p\) 的最大(不为本身) border,化归为子问题。
定理3(周期定理):若 \(p, q\) 为 \(s\) 的周期,\(\gcd(p, q)\) 也是 \(s\) 的周期。
证明:根据定义,\(s_i = s_{i + p} = s_{i + q}\)。
令 \(j = i + p\),则 \(s_j = s_{j + q - p}\),说明 \(q - p\) (\(q > p\))也是 \(s\) 的周期,而更相减损的最终结果就是 \(\gcd\)。
定理4:一个串的 border 数量是 \(O(n)\) 的,组成 \(O(n \log n)\) 个等差数列。
Next 数组
定义:\(ne_i\) 表示 \(pre_i\) 的最大非平凡 border,其中 \(ne_1 = 0\)。
不难发现 \(pre_i\) 的所有 border 减 \(1\) 一定是是 \(pre_{i - 1}\) 的 border(画图)。
这是一个必要条件,我们遍历 \(pre_{i - 1}\) 的所有 border 检查充分性:
依次检查 \(ne_{i - 1}, ne_{ne_{i - 1}}\cdots\) 的下一位是否等于 \(s_i\),以此递推出 \(ne_i\)。
时间复杂度分析:
- 如果 \(ne_i = ne_{i - 1} + 1\),势能(最大迭代次数)增加 \(1\),
- 否则会先迭代到某个 \(ne_j\),然后 \(ne_i = ne_j + 1\),势能不增。
- 否则 \(ne_i = 0\),势能清空。
综上,势能总量为 \(O(n)\),时间复杂度 \(O(n)\),常数大概在 \(2\) 左右。
题意:对于 \(s\),求最长的子串 \(t\),满足:\(t\) 是 \(s\) 的前缀;\(t\) 是 \(s\) 的后缀;\(t\) 在 \(s\) 中至少出现 \(3\) 次。
- 如果 \(ne_n\) 在中间出现过至少一次,那么答案就是 \(ne_n\)。
- 否则取 \(ne_{ne_n}\),次长 border 至少出现 \(4\) 次,满足限制条件。
题意:对于矩阵 \(A[m \times n]\),找到子矩阵 \(B[p \times q]\),满足 \(B\) 是 \(A\) 的二维周期,最小化 \((p + 1) \times (q + 1) \times \max B\)。
先最小化 \(p, q\),横周期与纵周期互相独立,\(p\) 为所有行的公共周期里最小的一个,\(q\) 同理。
然后就是问你 \(p \times q\) 里的最大值,单调队列求二维最值。
题意:给定 \(a[n]\) 和 \(b[m]\),问 \(a\) 有多少长度为 \(m\) 的区间 \(a'\) 满足 \(\forall i, j,\ a'_i + b_i \equiv a'_j + b_j \pmod k\)。
形式化上述条件:
题意:定义 \(f(s, t)\) 表示 \(t\) 在 \(s\) 中出现次数。给定 \(n\) 个串,对于每个 \(s_i\) 求出 \(\prod f(s_i, s_j)\)。
设这 \(n\) 个串中最短长度为 \(m\)。
如果 \(s_i\) 串长大于 \(m\),显然无法匹配一个长度为 \(m\) 的串,直接输出 \(0\) 即可。
所有长度为 \(m\) 的字符串答案都一样,任取一个匹配即可。
题意:求 \(s\) 有多少子串存在子串与 \(t\) 匹配。
设 \(s\) 与 \(t\) 匹配的位置右端点分别为 \(p_1, p_2, \cdots, p_k\)。
每个 \(p_i\) 只对最右边的匹配位置是 \(p_i\) 的区间有贡献,这样可以不重不漏。
Hash
定义
hash 是一种单射函数,可以将一些东西映射为一个整数值。
字符串哈希指讲字符串映射为一个整数值的方法,通常用来快速判等。
约定:\(H(S)\) 表示一个字符串 \(S\) 通过函数 \(H\) 映射成的整数值。
性质
必要性:如果 \(S = T\),一定有 \(H(S) = H(T)\)。
非充分性:如果 \(H(S) = H(T)\),不一定有 \(S = T\)。
Hash检测
通过检验 \(H(S), H(T)\) 是否相等,来判断 \(S = T\) 的方法。
Hash冲突
\(H(S) = H(T)\land H \ne T\),即发生了 Hash 冲突。
Hash 检测时发生 Hash 冲突的概率是评判 Hash 算法好坏的重要指标。
多项式Hash
将字符串看作某个进制(Base)下的数:
优点:字符串与 Hash 值一一对应,不会发生 Hash 冲突。
缺点:数字范围过大,难以存储。
多项式取模 Hash(模哈)
为了解决多项式 Hash 的缺点,在效率与冲突率之间折中,对一个大质数取模:
优点:可以用原始数据类型存储。缺点:小概率发生冲突。
模哈冲突概率
\(H(S) \ne H(T)\) 且 \(H(S) \equiv H(T)\)。
模运算可以看作一个均匀随机散列,即每个 \(H(S)\) 会被随机映射为 \([0, P)\) 的整数。
生日悖论
\(n > 365\):一定有人生日相同。
\(n \le 365\):没有人生日相同的概率为 \(\frac{A_{365}^n}{365^n}\)。
当 \(n = 23\) 时,上式约为 \(0.46\),冲突概率大于 \(0.5\)。
即当检验次数超过 \(\sqrt{\text{Mod}}\) 时,就有大概率发生错误。
因此,在模哈使用的 \(\text{Mod}\) 最好超过 Hash 检测次数的平方。
Hash模数
优秀的哈希模数首先应当满足足够大。
自然溢出:用 ull 存储哈希值,相当于自动对 \(2^{64}\) 取模,但容易构造哈希冲突。
优秀的哈希模数首先还应是一个质数。
所谓哈希冲突即出现 \(H(S) - H(T) \equiv 0\) 的情况(多项式零点)。
(选用质数的原因可能是使两边公约数尽可能少?)
单模:选取 \(10^9\) 到 \(10^{10}\) 范围的大质数作为 Hash 模数。但也有广为人知的方法构造冲突。
双模(多模):进行多次不同质数的单模哈希,在不泄露模数的前提下,没有已知方法构造冲突。
(必须两个质数都取到零点才出现冲突)
例1:子串哈希
设 \(F(i) = H(pre_i)\)。
那么
所以只要预处理所有前缀哈希,就能快速求出子串哈希。
例2:回文前缀
求 \(s\) 最长的回文前缀。
枚举长度 \(i\),检验正串 \(s\) 和其翻转 \(t\) 是否有 \(H_s(pre_i) = H_t(suf_i)\)。
例3:子串字典序比较
\(Q\) 次询问,每次查询 \(s[l_1, r_1]\) 和 \(s[l_2, r_2]\) 字典序大小。
等价于求解两个串的最长公共前缀(LCP),显然可以哈希 + 二分完成。
例4:子串字典序比较(单点修)
左右两串可以快速合并,线段树维护哈希值。
例5:子串字典序比较(区间修)
修改:将区间 \([l, r]\) 的位置加 \(1\)。
等价于在 \([l, r]\) 上加了一个等比数列 \(\{B^{r - l}, \cdots, B, 1\}\)。
如果这段区间被加了 \(k\) 次,那么他的贡献即 \(\dfrac{1 - B^{r - l + 1}}{1 - B} \times k\),维护区间加线段树。
题意:长度为 \(n\) 的字符串,支持两种操作:
- 将 \([l, r]\) 赋值为 \(k\)。
- 询问 \(p\) 是否为 \([l, r]\) 的周期。
等价于询问 \([l, r]\) 是否有 \(len - p\) 的 border,哈希判定前后缀相等。
修改直接在线段树上维护一个区间推平的 tag 即可。
P4324 [JSOI2016] 扭动的回文串 *提高+/省选-
P5685 [JSOI2013] 快乐的 JYY *省选/NOI-
题意:求数对 \((l, r, x, y)\) 个数,满足:
- \(A[l, r] = B[l, r]\)。
- \(A[l, r]\) 是回文串
一个回文串 \(s\) 对答案的贡献为 \(s\) 在 \(A\) 中出现的次数乘上 \(B\) 中出现的次数。
问题转化为求每种回文串的出现次数。
结论:一个串的本质不同回文串只有 \(O(n)\) 个。
所以可以枚举回文中心,先求出 \(2n\) 个极长回文串,如果当前串没有出现过,往中间缩减。
求出所有本质不同串后如何统计答案?
一个极长回文 \(s\) 会对 \(s, s[2, n - 1], s[3, n - 2]\) 产生 \(1\) 的贡献。
那么可以 \(s[2, n - 1]\) 连向 \(s\),所有本质不同串形成森林,直接树上差分统计答案。
具体来说,\(x\) 会贡献 \(x\) 到根的一条链,在当前点加一即可。
(出题人应该把我所有能想到的单模卡了,所以要双模)
Trie
几个定义:
- 一个字符串的集合称为字典。
- 在字典中的串称为字典串。
- Trie 是一棵有根树,每个点至多有 \(\vert\sum\vert\) 个后继边,每条边上有一个字符。
每个点表示一个前缀:从跟到这个点的边上的字符顺次连接形成的字符串。
每个点还有一个终止标记:是否这个点代表的字符串是一个字典串。
trie 支持插入,删除,以及其他复杂查询。
trie 的持久化与线段树类似,新树的形态改变不大(点名批评平衡树)。
oi 中常用 01-trie 处理有关 01 串和位运算等信息,顾名思义,01-trie 的 \(\sum = \{0, 1\}\)。
题意:给定 \(n\) 个互不相同的字符串,你可以重新定义字典序,求那些串可能成为字典序最小?
建出 trie,对每个串依次检查。
设当前走到了 \(x\) 这个节点,我们要从 \(x\) 向与 \(x\) 同一父亲的节点(且存在)连边,表示 \(x\) 的字典序比这些字母小。
最后会得到一张有向图,如果图中有环则产生矛盾,可以拓扑排序判断。
但是这样会漏掉 abcxxx
和 abc
的大小判断,认为的在每个串后面加一个字符 '0',且 '0' 小于任何其他字符。
题意:给定数组 \(a\),求 \(\max\limits_{l \le r}\bigg(\bigoplus\limits_{i = l}^r a_i \bigg)\)。
每次插入一个前缀异或,同时贪心的找该前缀的异或最大值。
题意:\(m\) 个询问,每次查询 \(l\le p \le r\) 里最大的 \(x \oplus a_p \oplus a_{p + 1} \cdots \oplus a_n\)。
可持久化 trie 板子。
发现要在后面添数,用后缀不好做,转化为 \(x \oplus pre_n \oplus pre_{p - 1}\)。
显然可以持久化满足 \(r\) 的限制。
然后对每个点记录当前版本经过这个点的最大前缀编号就能满足 \(l\) 的限制。
Border 树
定义
对于一个字符串 \(s\),他的 Border 树(失配树)共有 \(n + 1\) 个节点。
\(0\) 是这颗有向树的根。对于 \(1 \le i \le n\),\(ne_i\) 是 \(i\) 的父节点。
性质
- 前缀 \(pre_i\) 的所有 border:\(i\) 到根的链。
- 哪些前缀含有 border \(x\):\(x\) 的子树。
- 求两个前缀的公共 border 等价于求 lca。
P5829 【模板】失配树 *提高+/省选-:submission
题意:求 \(s\) 的最长子串 \(t\),满足:
- \(t\) 同时是 \(s\) 的前后缀。
- \(t\) 在 \(s\) 中至少出现 \(k\) 次。
考虑在 border 树上这两个条件等价于什么。
\(t\) 是 \(s\) 的 border,即在 \(n\) 到根的链上;至少 \(k\) 个前缀含有 border \(t\),子树大小不小于 \(k\)。
我们注意到 \(x\) 的父亲一定比 \(x\) 小,所以不需要显式建树,直接从后往前递推子树大小。
询问与上题一致,支持在字符串后面添加一个字符。
首先考虑优化查询,一条链上的子树大小是单调递增的,可以类比 lca 用倍增维护。
现在要动态求子树大小,把所有修改离线,维护最终树的 dfs 序,新增 \(x\) 就相当于把 \(x\) 的权值由 \(0\) 改 \(1\),树状数组维护区间和。
AC 自动机
背景
给出一个字典,和若干询问:多少个字典串在询问串中出现过。
即单串与多串的匹配问题。
广义 border
对于串 \(s\) 和一个字典 \(D\),相等的 \(p\) 长度的 \(s\) 的后缀,和任意一个字典串 \(t\) 的前缀称为一个 border。
失配指针
对于 trie 中的某一节点(某一前缀 \(s\)),
其最长非平凡 border 在 trie 中对应的位置(\(s\) 在字典树中出现的最长后缀)即失配指针。
类比 kmp,一个点的 border 减一一定是其父亲的 border,因此不断跳父亲的失配链检验充分性,复杂度均摊线性。
一个点的失配指针取决于其父节点的失配链,如果用 dfs 更新,有可能跳到未遍历的子树,因此 bfs 一层一层更新。
题意:给你一个文本串 \(s\) 和 \(n\) 个模式串 \(t_i\),请你分别求出每个模式串 \(t_i\) 在 \(s\) 中出现的次数。
把文本串放在 ac 自动机上跑会发生什么情况?
如果当前匹配到 \(pre_i\),对应 ac 自动机上的节点 \(x\),说明根到 \(x\) 的字符串是 \(pre_i\) 的极长后缀。
匹配 \(pre_{i + 1}\) 只要不断跳 fail 链然后检验即可。
考虑如何得到题目要求的答案,
以右端点为标识统计子串,换句话讲,对于每个 \(pre_i\) 求出匹配的后缀并计入答案。
文本串在 ac 自动机上已经跑到了极长后缀 \(x\),
根据 border 的传递性,\(pre_i\) 的其他后缀都是 \(x\) 的 border,即失配树上 \(x\) 到根的一条链。
因此可以在 \(x\) 标记加一,最后统计失配树上的子树标记和。(不难看出,ac 自动机 = 字典树 + 失配树)
代码在理解后是极好写的:submission
题意:给定字典,允许往里加字符串,每次询问文本串里模式串出现次数。
离线修改,建最终的 ac 自动机。
对于询问,每次 ac 自动机匹配到一个点就把该点到失配树的根之间标记数量计入答案。
具体来讲,\(x\) 上的标记会对 \(x\) 子树的查询有贡献,树状数组维护差分数组。
现在瓶颈在于一次询问是 \(O(n + m + m\log N)\) 的,\(q\) 次查询无法接受。
令 \(nxt_{x, ch}\) 表示当前在 ac 自动机的点 \(x\),文本串的下一个字符是 'ch' 时会跳到哪个点。
- 如果 \(x\) 有后继边 \(ch\):\(nxt_{x, ch} = to_{x, ch}\)。
- 否则跳到 \(fail_x\) 继续尝试,\(nxt_{x, ch} = nxt_{fail_x, ch}\)。
trie 图就是边集等于 \(x \to f_{x, ch}\) 的一张图(DFA),这样就把跳 fail 链的 \(O(n)\) 复杂度给优化掉了。
同时我们可以借助 \(nxt\) 数组递推出 \(fail\),对于 \(x\) 经过 \(ch\) 连向的点 \(y\):
P7456 [CERC2018] The ABCD Murderer *省选/NOI-
题意:给定 \(n\) 种字符串 \(t\),问最少需要多少个字符串才能拼出 \(s\)。这里的拼接允许相邻字符串的相同前后缀重合。
设 \(f_i\) 表示长度为 \(i\) 的前缀的最小代价,那么对于任何 \(pre_i\) 的后缀 \(s\),有:
ac 自动机维护与前缀匹配的最长后缀,设当前匹配到 \(x\),那么上式中的 \(\vert s \vert\) 即 \(x\) 到根的链上标记(字典串长)取 \(\max\)
P4052 [JSOI2007] 文本生成器 *省选/NOI-
题意:给定 \(n\) 个串,问至少包含其中之一作为子串的长度为 \(m\) 字符串数量。
正难则反,转换为 \(26^m\) 减去一个串都不包含的方案数。
\(f_{i, x}\) 表示以长度为 \(i\) 匹配到 ac 自动机的 \(x\) 节点上的合法方案数,
如果 \(x\) 到根(fail 树)直接存在字典串,\(f_{i, x} = 0\)。
题意:判断是否存在一个无限长的 01 串不包含任意一个给定字符串。
把文本串放到 trie 图上跑,把包含给定字符串的点标记为非法,判定条件等价于不经过非法点是否存在一个根能到达的环。
题意:求 \(1 \sim n\) 有多少数不包含给定字符串。
ac 自动机强行套数位 dp,状态为 \(f(pos, x, lim, lead)\),表示前 \(pos\) 位,匹配到点 \(x\),有无高位限制,有无前导零。
实际的数字不存在前导零,如果 \(lead = \text{ true}\),不能跳到 \(nxt_{0, 0}\) 而应待在 \(0\) 不动。
P2414 [NOI2011] 阿狸的打字机 *省选/NOI-
题意:给定 \(n\) 个串,询问 \((x, y)\) 表示 \(x\) 在 \(y\) 里出现多少次。
也就是求有多少个 \(y\) 的前缀的 fail 链经过 \(x\) 的终止节点,即 fail 树上有多少 \(y\) 的前缀在 \(x\) 终止节点的子树里。
设 \(ed_i\) 表示第 \(i\) 个位置对应 trie 上的节点编号,为了方便计算,把询问 \((x, y)\) 改写成 \((ed_x, ed_y)\)。
设每个点在 fail 树上的 dfs 序为 \(in_x\) 和 \(out_x\)。
dfs 整棵 trie,每次进入时将 \(in_x\) 位置加一,出去时 \(in_x\) 还原,这样树状数组中只存了根到 \(x\) 的一个前缀。
遍历所有关于 \(x\) 的询问点(这里 \(x\) 等效于上文的 \(ed_y\))\(q\),\([in_q, out_q]\) 的区间和即该询问答案。
P6257 [ICPC2019 WF] First of Her Name *省选/NOI-
题意:以字典树的形式给出 \(n\) 个串,每次查询有多少串的后缀包括询问串。
如果 \(t\) 是 \(s\) 的后缀,那么 \(t\) 一定在 \(s\) 的 fail 链中,因此离线询问串,统计子树和。
题意:\(f(s, t)\) 表示 \(t\) 在 \(s\) 中的出现次数,每个询问 \((l, r, k)\) 表示 \(\sum_{i = l}^r f(s_i, s_k)\)。
等价于统计 \(s_k\) 的终止节点在 fail 树上的子树里有多少 \(l \sim r\) 的前缀。
把询问离线,拆成前缀之差,做一遍扫描线。
题意:\(L \sim R\) 之间有多少数字至少包含一个长度不小于 \(\lfloor\frac{d}{2}\rfloor\) 的 \(s\) 的子串,\(n \le 1000,\ \lg R < 51\)。
首先你 \(\lfloor\frac{d}{2}\rfloor + 1\) 都满足了,\(\lfloor\frac{d}{2}\rfloor\) 肯定也满足,所以只要考虑所有长度为 \(\lfloor\frac{d}{2}\rfloor\) 的子串。
暴力建树,然后做数位 dp。submission
后缀数组
复习一些定义:
后缀:\(suf_i = s[i, n]\)。
字典序:对于 \(s\) 和 \(t\),从左往右找到第一个不同的位置然后比较,实际是一个找 lcp 的过程。
后缀排序:把每个 \(suf_i\) 当做独立的串,升序排序。
后缀排名:\(rk_i\) 表示 \(suf_i\) 在后缀排序中的排名,即它是第几小的后缀。
后缀数组:\(sa_i\) 表示排名第 \(i\) 小的后缀(\(rk_{sa_i} = i\))。
Naive Sort
二分加哈希求 lcp,定义两个字符串的比较函数,直接调 sort,时间复杂度 \(O(n \log^2 n)\)。
缺陷:哈希检测次数高达 \(O(n\log^2 n)\) 次,非常容易冲突,且复杂度过高。
前缀倍增法
首先等效的在字符串末尾加无穷多空字符串 \(\varnothing\)。
定义 \(s(i, k)\) 表示从 \(i\) 开始长度为 \(2^k\) 的字符串,那么只要将所有 \(s(i, \lceil\log_2{n} \rceil)\)。
考虑如何通过 \(s(i, k)\) 的排序结果(rk和 sa)得到 \(s(i, k + 1)\) 的排序结果。
可以将 \(s(i, k + 1)\) 看成一个两位数,高位为 \(rk\big(s(i, k)\big)\),低位为 \(rk\big(s(i + 2^k, k)\big)\)。
两位数怎么排?基数排序 \(O(n)\)。
-
首先统计 \(cnt_x\),表示高位 \(A = x\) 的数字有多少个。
可以确定第 \(i\) 个数字的最终排名一定在 \(\sum_{x< A_i} cnt_x\) 与 \(\sum_{x \le A_i}cnt_x\) 之间。记为 \(\big(s_{A_{i − 1}}, s_{A_i}\big]\)。
完成了数字的分块,以及块与块之间的排序。
-
接下来需要确定每个块(\(A = x\))内数字的顺序。
按照低位 \(B\) 从大到小,依次地领取排名 \(s_{x}, s_x - 1, \cdots , s_{x - 1} + 1\)。
因此这一步需要事先对把所有数字按照低位排序。(实现上是先排低位,后排高位。)
时间复杂度 \(O(n\log n)\),基数排序有 \(5 \sim 6\) 的常数(我板子写了 \(10\) 倍常数)。
区间可加性
假设有一组排序过的字符串 \(A = [A_1, A_2, \cdots, A_n]\),如何快速求任意 \(A_i, A_j\) 的 lcp?
对于任意 \(k \in [i, j]\):
不难得到,\(\text{lcp}(A_i, A_j) = \min\limits_{k = i}^{j - 1}\big(\text{lcp}(A_k, A_{k + 1})\big)\)。
证明:
-
\(\text{lcp}(A_i, A_k)\ne \text{lcp}(A_k, A_j)\)
由于 \(Z = Y\) 且 \(Y \ne X\),因此 \(Z \ne X\)。(这里是小于的情况,大于同理)
-
\(\text{lcp}(A_i, A_k)= \text{lcp}(A_k, A_j)\)
由于 \(X < Y < Z\),因此 \(Z \ne X\)。
Height 数组
\(\text{height}_i\) 表示后缀 \(i\) 与排名在他前面一个的后缀的 lcp,即
结论 \(\text{height}_i \ge \text{height}_{i - 1} - 1\)。
证明:记 \(K_1 = rk_{i - 1} - 1,\ K_2 = rk_i - 1\)。
如果 \(\text{height}_{i - 1} \le 1\),由于 lcp 非负,结论显然。
否则 \(\text{height}_{i - 1} > 1\),
砍掉两个后缀的首字母,
有 \(suf_{K_1 + 1} < suf_i\) 且 \(\text{lcp}(suf_{K_1 + 1}, suf_i) = \text{height}_{i - 1} - 1\)。
由于 \(K_2\) 是小于 \(i\) 的第一个后缀,因此 \(K_2\) 的字典序在 \(K_1 + 1\) 与 \(i\) 之间。
根据区间可加性,\(h_{i - 1} - 1 = \min\big( \text{lcp}(suf_{K_1 + 1}, suf_{K_2}),\ \text{height}_i \big)\),所以有 \(\text{height}_i \ge \text{height}_{i - 1} - 1\)。
如何求 \(\text{height}_i\)?
首先让 \(\text{height}_i\) 继承 \(\text{height}_{i - 1} - 1\),然后暴力延伸。势能减少量 = 势能增加量 = \(O(n)\)(最多减 \(n\) 次,上界最大到 \(n\))。
实际使用时常用 \(h_i = \text{height}_{sa_i} = \text{lcp}\big(s[sa_i, n]\, s[sa_{i - 1}, n]\big)\)。SA模板测试:submission
题意:求字符串 \(s\) 本质不同子串数,\(\vert s\vert \le 50000\)。
一种枚举子串点方式是枚举所有后缀点所有前缀。
按字典序枚举后缀,假设后缀排序后形成的字符串集合为 \(\{A_1, A_2,\cdots ,A_n\}\)。
假设当前枚举到 \(A_i\),其前缀数共 \(n - sa_i + 1\) 个,现在要减去出现在前 \(i - 1\) 个后缀里的前缀。
由于是按字典序排序的,不存在一个字符串是 \(A_{j < i - 1}\) 和 \(A_i\) 点公共前缀而不是 \(A_{i - 1}\) 和 \(A_i\) 的公共前缀。
因此 \(A_i\) 对答案是贡献即 \(n - sa_i + 1 - h_i\)。
题意:给出只由 \(abc\) 组成的字符串,求有多少置换意义下本质不同子串。
如果两个串在 \(\{a,b,c\}\) 的某种置换作用下相等,则认为本质相同(如 "aba" 和 "cac")。
字符集很小,不妨枚举所有 \(6\) 种置换。
将这 \(6\) 种字符串通过不同拼接字符(如 '#''$''%')连接起来(特殊字符的目的即防止出现跨串的公共前缀)。
考虑求出拼接串的不包含拼接字符的本质不同子串数 \(A\)。
由于确保了拼接字符互不不同,任意包含拼接字符的子串都是本质不同的,\(A\) 可以简单容斥求得。
考虑每种置换意义下本质不同串对 \(A\) 有多少贡献:
- 如果 \(s\) 中至少存在两个不同字符,则 \(s\) 贡献 \(6\) 种本质不同串。
- 如果 \(s\) 只有一种字符,则 \(s\) 贡献 \(3\) 种本质不同串。
假设原字符串存在 \(B\) 种长度的连续相同子串,那么最终答案即 \(B + \frac{A - 3B}{6}\)。
题意:给出两个字符串 \(s, t\),求有多少个长度大于 \(k\) 的公共子串。
用特殊字符拼接 \(s, t\),求出对应的 \(h\) 数组。
\(O(n^2)\) 做法是枚举子区间 \([l, r]\) 如果 \(sa_l, sa_r\) 分属不同字符串,将 \(\min\big[h_{l + 1}\cdots h_r\big] - k + 1\) 计入答案。
考虑单调栈优化这一过程。
如果 \(sa_i\) 属于 \(s\),把左侧 \(t\) 的贡献计入答案;如果 \(sa_i\) 属于 \(t\),把左侧 \(s\) 的贡献计入答案。
这里只讨论第一种情况。
设 \(cnt_j\) 表示以 \(h_j\) 作为 \(\text{lcp}(\min\big[h_{x + 1}\cdots h_i\big])\)的点 \(x\) 数量,且 \(sa_x\) 属于 \(t\)。
如果 \(h_i\) 小于等于当前栈顶,出栈,\(i\) 继承栈顶的 \(cnt\)。
\(i\) 对答案点贡献为 \(\sum cnt_j \times (h_j - k + 1)\),在出入栈时同时维护这个总和即可。
题意:求 \(s, t\) 的最长公共子串。
还是和上题一样,特殊字符连接 \(s\) 和 \(t\),求出 \(h\) 数组。
枚举每个 \(i\) 满足 \(sa_i\) 属于 \(t\),找到左右两边第一个 \(s\) 后缀出现点位置 \(l, r\)。
那么 \(i\) 的贡献即 \(\max\big(\text{lcp}(l, i),\ \text{lcp}(i, r)\big)\),枚举所有 \(i\) 取最大值。
为什么找左右出现的第一个位置?对于 \(l' < l\),一定有 \(\text{lcp}(l', i) \le \text{lcp}(l, t)\),显然不优。
题意:求 \(s\) 和 \(t\) 的本质不同公共子串数。
按字典序枚举 \(t\) 的后缀,每次产生 \(n - sa_i + 1 - h_i\) 个新的 \(t\) 的前缀。
对于这个后缀 \(sa_i\),与 \(s\) 的最长公共子串显然可以用上题的方法求出,记为 \(\text{maxlen}\)。
在比他小的后缀里有 \(h_i\) 个已经出现的前缀,那么在 \(i\) 的贡献里重复出现的公共子串即 \(\min(h_i,\ \text{maxlen})\)。
P6257 [ICPC2019 WF] First of Her Name *省选/NOI-
题意:以字典树的形式给出 \(n\) 个串,每次查询有多少串的后缀包括询问串。
树上 sa,点 \(x\) 表示从 \(x\) 到根的字符串。
对所有节点代表的字符串排序,同样是倍增,只不过 \(s(x, k)\) 表示从 \(x\) 往上跳 \(2^k\) 步的字符串。
在线处理每个询问,答案区间在排序后的序列里一定是连续的,二分即可。
二分可以直接暴力比较字典序,一次 check 最多花费询问串长,而询问串总和是有保证的。
预处理(树上 \(k\) 级祖先,sa)以及询问的复杂度都是线性对数。
submission?(待补)
P3181 [HAOI2016] 找相同字符 *省选/NOI-
题意:求 \(s, t\) 的公共子串对数。
严格弱于 poj 那道,拼接后求出 \(h\) 数组,然后单调栈扫一遍。
题意:记 \(t_i\) 为 \(s\) 从 \(i\) 开始的后缀,求 \(\sum\limits_{i < j} \text{len}(t_i) + \text{len}(t_j) - 2 \times \text{lcp}(t_i, t_j)\)。
前面一部分为 \((n - 1) \times \dfrac{n(n + 1)}{2}\),后面一部分即求 \(h\) 数组所有区间的 \(\min\) 和。
我们固定右端点 \(i\),这一部分贡献可以当做新增一个 \(h_i\) 并对 \(i - 1\) 的答案取 \(\min\),平衡树维护。
submission(学傻了,可以直接单调栈)
后缀自动机
定义
\(s\) 的后缀自动机是一种能够识别所有 \(s\) 的子串的自动机类型的数据结构(确定性有限状态自动机,DFA)。
自动机 = 状态集合(state)+ 状态转移(trans)+ 起始状态 + 终止状态 + 字符集。
Naive 版本
将 \(s\) 的的 \(n\) 个后缀看作独立的串建 trie 树,可以识别所有后缀的前缀,即所有子串。
缺陷:状态数和转移数都为 \(O(n ^ 2)\)。
最简状态后缀自动机
- \(s(w)\) 表示子串 \(w\) 对应的后缀自动机上的状态。
- \(\text{trans}(s, ch)\) 表示当前状态是 \(s\),接收新字符 \(ch\) 之后到达的状态。
- \(\text{Trans}(s, str)\) 表示当前状态是 \(s\),接收新字符串 \(str\) 之后到达的状态。
- \(suf_i\) 表示从 \(i\) 开始的后缀。
假设某个子串 \(t\) 属于某个状态 \(s = s(t)\),设 \(t\) 出现位置的右端点分别为 \(r_1, r_2, \dots, r_m\)。
则 \(s\) 一定能接收 \(suf_{r_i + 1}\)(可以走到一个有意义的状态表示 \(t\) 与 \(suf_{r_i + 1}\) 拼接成的后缀)。
如果存在另一个子串 \(t'\) 所在的状态 \(s\) 也只能接收 \(suf_{r_1 + 1},\ suf_{r_2 + 1},\cdots ,suf_{r_m + 1}\),
即 \(s\) 与 \(s'\) 所能接收的输入相同,从 DFA 的角度看,他们必须满足 \(s = s'\)。
不难发现 \(t'\) 是 \(t\) 的后缀(或者反过来),因此后缀自动机中的一个状态表示的是位置相同长度不同的一系列串。
定义 \(\text{Right}(s) = \{r_1, r_2, \dots, r_m\}\),表示 \(s\) 状态代表子串的右端点位置(endpos 叫法更常见)。
推论:
在最简状态后缀自动机中,所有节点的 \(\text{Right}\) 互不相同。
每个节点代表的串之间构成后缀关系,即所有串都是最长串的后缀。
每个节点代表的串长是一个连续区间,记为 \(\big[\text{MinL}(s),\ \text{MaxL}(s)\big]\)。
考虑同在 \(\text{Right}(s)\) 长度为 \(\text{MinL}(s) - 1\) 的子串 \(t\),一定有 \(\text{Right}(s) \subsetneq \text{Right}(s(t))\)。
Right 的性质
设两个状态 \(s, s'\),其 \(\text{Right}\) 集合分别为 \(R(s)\) 和 \(R(s')\)。
假设 \(R(s) \cap R(s') \ne \varnothing\),并且 \(r \in R(s) \cap R(s')\)。
由于每个节点代表的串长是一段连续区间,不失一般性,可以认为 \(s\) 表示的串都比 \(s'\) 表示的长。
因此必然有 \(R(s)\subsetneq R(s')\)。
结论:两个不同状态的 \(\text{Right}\) 集合要么真包含,要么相离。即形成一种树形结构。
证明不同状态数是线性的。
对于所有度数为 \(1\) 的节点 \(x\),设他唯一的儿子设为 \(y\)。
设 \(R(z) = R(x) - R(y) \ne \varnothing\),把 \(z\) 同样挂在 \(x\) 上,这样我们就构造出了一棵每个点的度数至少为 \(2\) 的树(除叶子)。
由于最多有 \(n\) 个叶子,树中节点数不超过 \(2n - 1\)。(对应一棵满二叉树,\(\texttt{abbb}\cdots \texttt{abbb}\) 可以达到这个上界)
后缀链接树
\(\text{suffix-chain}\) 树(后缀链接树)即为 \(\text{Right}\) 集合包含关系形成的树。
定义 \(f(s)\) 表示状态 \(s\) 对应的 \(\text{Right}(s)\) 在 SC 树上的父节点
性质:
-
每个前缀所在的状态两两不同。
-
任意串 \(w\) 的后缀全部位于 \(s(w)\) 的后缀链接路径上(走到根)。
-
如果某一状态 \(s\) 有 \(ch\) 的转移边,则 \(f(s)\) 也有 \(ch\) 的转移边(不一定转移到同一个状态)。
-
对于任意状态 \(s\),\(\text{MaxL}\big(f(s)\big) = \text{MinL}(s) - 1\)。因此每个状态额外记录 \(L(s) = \text{MaxL}(s)\)。
因此 \(s\) 能表示的长度范围为 \(\big(L(f(s)),\ L(s)\big]\)。
-
所有终止节点都能代表至少一个后缀。
证明转移数是线性的。
考虑以源点为根的一棵生成树,由于节点数最多 \(2n - 1\),所以树边也是 \(O(2n)\) 条。
对于非树边 \((u, v)\):
- 从根延树边走到 \(u\)。
- 走 \((u, v)\),随便在自动机上走到一个后缀(终止节点)。
由于从根到一个后缀的路径是唯一的,\((u, v)\) 对应路径上的第一条非树边,这样就形成了一一映射,因此非树边也是 \(O(n)\) 的。
总转移数等于 \(O(3n)\)。
在线增量构造
首先创建第一个节点 \(1\) 表示根(空子串 \(\varnothing\)),\(L(1) = 0,f(1) = \text{null}\),且 \(R(1)\) 为全集。
逐个读入 \(S\) 的每个字符 \(S_i\),对已有的 \(S[1, i − 1]\) 的自动机进行一系列调整,得到 \(S[1, i]\) 的自动机。
考虑 \(S[1, i]\) 的自动机与 \(S[1, i − 1]\) 的自动机有什么差别?
新增了 \(i\) 个子串,即 \(S[1, i]\) 的所有后缀。
我们要确保这些新子串都能找到对应的状态,并且保证这些状态的转移边是完备的,
即每个新子串都可以从根沿转移边走到,同时要保证每个状态的 \(f, L\) 值正确。
创建新状态
从长到短考虑 \(S[1, i]\) 的 \(i\) 个后缀。
\(S[1, i]\) 一定是原自动机中不存在的串,必须要新建一个状态 \(np\),其中 \(L(1) = i,\ R(1) = \{i\}\)。
对 \(np\) 而言,不确定的有 \(f(np)\) 以及哪些状态需要增加 \(\text{trans}(s, S_i) = np\) 的转移边。
寻找父节点
由于 \(R(np) = \{i\}\),要找最长的 \([1, i - 1]\) 也出现过的 \(S[1, i]\) 的后缀 \(S[i - x + 1, i]\)。
\(S[i - x + 1, i] = S[i - x + 1, i - 1] + S_i\),因此需要枚举 \(S[1, i - 1]\) 的后缀。
设 \(p = s(S[1, i - 1])\),这一步可以通过跳 \(p\) 到根的链实现:
-
\(p\) 到 \(root\) 的链上所有状态都没有 \(S_i\) 的转移边。
即所有 \(i\) 个新串都只在 \(i\) 出现,因此都可以被 \(np\) 表示,此时 \(f(np) = root\)。
-
找到了第一个有 \(S_i\) 转移边的状态 \(p'\)。
这意味着我们找到了 \(np\) 无法表示的最长的 \(S[1, i]\) 后缀,长度为 \(L(p') + 1\)。
那么此时 \(f(np)\) 应该是什么呢?是 \(q = \text{trans}(p', S_i)\) 吗?
补全转移边
由于所有新串都是 \(S[1, i]\) 的后缀,即 \(S[j, i] = S[j, i − 1] + S_i\),
因此我们需要确保 \(S[1, i − 1]\) 的每个后缀所在的状态 \(s\),都要有 \(\text{trans}(s, S_i)\) 转移边。
从长到短遍历 \(S[1, i − 1]\) 的后缀等价于在 SC 树上从 \(p\) 跳到 root:
显然,我们这样做会有两种结果:
如果链上所有状态都没有 \(S_i\) 的转移边。我们要做的只是给他们补上 \(\text{trans}(s, S_i) = np\)。
否则可以找到第一个带有 \(S_i\) 转移边的状态 \(p′\)。
本来要给 \(p′\) 添加指向 \(np\) 的 \(S_i\) 转移边,但是已存在 \(\text{trans}(p′, S_i) = q\) 了,怎么办呢?
-
\(L(q) = L(p') + 1\)。
双箭头代表树边,单箭头代表转移边。
对于可以用 \(np\) 表示的新串(等价于没有 \(S_i\) 边),向 \(np\) 连一条 \(S_i\) 的转移边。
根据前面的分析,\(np\) 不能表示的最长串长即 \(x+ 1\),直接令 \(f(np) = q\) 即可。
-
\(L(q) \ne L(p') + 1\)。
即 \(L(q) > L(p') + 1\)(不可能小于,因为 \(q\) 的最大长度一定不小于 \(p'\) 的最大长度加一条 \(S_i\) 边)。
\(np\) 的父节点必须满足 \(L(f(np)) = x + 1\),不能直接把父亲设为 \(q\)。
我们把 \(q\) 分裂出 \(nq\) 表示长度较小的部分,其中 \(L(nq) = x + 1\)。
\(nq\) 一定可以继承 \(q\) 的转移边(本来就是一个点)。由于 \(nq\) 比较短,不难得到 \(f(q) = nq\)。
除了将 \(q\) 的转移边复制给分裂状态 \(nq\) 外,还需调整一系列 \(p'\) 后缀链接上的 \(\text{trans}(s, S_i) = q\) 变为 \(\text{trans}(s, S_i) = nq\)。
因为 \(p'\) 有 \(S_i\) 的转移边,\(p'\) 到根的链都有 \(S_i\) 的转移边,且指向 \(q\) 到根的一条链。
设 \(p^*\) 为第一个不连向 \(q\) 的 \(p'\) 的祖先结点。
\(nq\) 分配了 \(\big(L(f(q)), x + 1\big]\) 的长度,因此从 \(p'\) 开始往上到 \(p^*\) 都要改连向 \(nq\)(即蓝色边改连成橙色边)。
对于 \(L \in (x, y)\) 的状态 \(s\),加上 \(S_i\) 的长度至少为 \(x + 2\),仍然连向 \(q\)。
最后令 \(f(np) = nq\)。
求最小循环串
题意:每次操作可以将 \(s\) 最前面一个字符移动到末尾,求可以得到的字典序最小串。
令 \(s' = s + s\),在 \(s'\) 的 sam 上走 \(n\) 步,每步选最小的那条转移边。
一定不会少于 \(n\) 步就走到终止节点。
如果走到了终止节点,说明走过的这一段路径对应右边 \(s\) 的一个后缀,而显然可以从左边的这个后缀继续往后走。
求 Right 集合大小
每加入一个新字符,就在这个新状态 \(s\) 上打上标记 \(1\)。最后在后缀链接树上求子树和。
首先 \(s\) 一定能表示前缀 \(i\),
所有在 \(i\) 出现的串都是前缀 \(i\) 的后缀,也就是所有 \(i\) 需要贡献的节点都在 \(s\) 到根的链上。
可以显式的建出后缀链接树,缺点是常数稍大。
我们可以根据拓扑序更新 dp 值,而后缀链接树的拓扑序取决于 \(L(s)\) 的大小,基数排序解决。
P3804 【模板】后缀自动机(SAM):submission
题意:定义 \(f(x)\) 为 \(s\) 的长度为 \(x\) 的子串最大出现次数,求 \(1 \sim n\) 的每个 \(f(x)\)。
自动机的每个节点对应一个等价类:恰好出现在 \(R(s)\) 的所有位置。
对于 \(\big(L(f(s)), L(s)\big]\) 的长度都存在出现过 \(\vert R(s)\vert\) 次的串,于是将 $\vert R(s)\vert $ 与这一区间的答案取 \(\max\) 即可。
线段树不能通过 149ms 的时限,观察到上述条件对 \([1, L(s)]\) 同样成立,于是只在 \(L(s)\) 记录,最后倒序取 \(\max\)。
快速定位子串对应状态
题意:\(q\) 次询问,每次询问子串 \(s(l, r)\) 在 \(s\) 中的出现次数。
sa 做法:排序,找到后缀 \(l\),往左往右找 lcp 不少于 \(r - l + 1\) 的边界。
如果使用 sam,那么就快速找到 \(s(l, r)\) 在后缀自动机中的位置。
先找到前缀 \(pre_r\) 在 sam 中的位置,然后树上倍增,单次复杂度 \(O(\log n)\)。
加强版:\(q\) 次询问,每次询问子串 \(s(l, r)\) 在 \(s(L, R)\) 中的出现次数。
等价于问 Right 集合里有多少 \([L + r - l, R]\) 出现的。
通过 dfs 序转化为子树查询,主席树可以做到强制在线,也可以离线询问 + 启发式合并。
与动态规划结合
题意:
你有一台打字机,你需要用它打出一段只由小写字母构成的文本 \(S\)。
设某个时刻,你已经打出来的文本为 \(T\),你可以执行两种操作:
- 花费 \(p\) 块钱,将 \(T\) 后面添加任意字符 \(ch\),得到 \(T′ = T + ch\)。
- 花费 \(q\) 块钱,将 \(T\) 的一个子串 \(S\) 添加到 \(T\) 的后面,得到 \(T′ = T + S\)。
问你为了得到 \(S\),最少的花费是多少,\(\vert S\vert \le 2\times 10^5\)。
设 \(f_i\) 表示匹配 \(S\) 的前 \(i\) 位的最小代价。
操作一对应转移 \(f_i \gets f_{i - 1} + p\)。
对于操作二,需要找到 \(pre_i\) 的最长后缀 \(S[i - x + 1, i]\) 使得该后缀在 \(pre_{i - x + 1}\) 也出现过。
对应转移:\(f_i \gets \min(f_{i - x,\ i - x,\cdots, i - 1 }) + q\)。
设 \(s\) 代表 \(S[i - x + 1, i]\) 的状态,条件等价于 \(s\) 的 Right 集合最小值不大于 \(i - x\)。
在 \(i\) 的后缀链接上倍增,找到深度最大的满足 \(\min\big(R(s)\big) + \text{MinL}(s)\le i\) ,然后再找到该状态最大满足条件的长度。
单模式匹配