本文是 NOI 一轮复习的第六篇,包括各类字符串算法。
前置知识
我们记字符集为 ,字符串是由若干字符集中的元素构成的序列。
字符串哈希
即序列哈希,快速比较两个序列的相等情况。一般来讲我们采用 进制方式的哈希,即 。
inline u64 q(u64 *f, int l, int r) { return f[r] - f[l - 1] * p[r - l + 1]; }
字典树
本质上是一种自动机结构,不再赘述。
字符串周期结构
定义 是串 的周期,当且仅当 且 ,如果满足 则称 是 的整周期。
称 是串 的 Border,当且仅当 是 的前缀且 是 的后缀,但是 。
结论:若 是 的周期,当且仅当 是 的 Border。
周期引理:若 是 的周期,且 ,那么 是 的周期。
短周期结构:根据周期引理,对于长度不超过一半的周期,都有其 是周期,那么字符串的所有长度不超过其一半的周期都是其最短周期的倍数。
Border 结构:字符串的所有周期或 Border 形成了 个值域不相交的等差数列。
求字符串 的所有周期长度的线性组合可以组成多少个 内的数。
拆成 个等差数列,直接使用 Dijkstra 做同余最短路。在转移一个等差数列时,如果在 (首项)意义下不优,则直接 break
掉。时间复杂度 ,代码。
KMP 算法
KMP 算法用于求出 数组:当前位置的最长 Border。求解过程使用增量法:如果当前位置失配,则跳到上一个匹配成功的位置。匹配成功的位置为其势能,因此时间复杂度为 。
for (int i = 2, p = 0; i <= m; ++i) {
while (p && s2[i] != s2[p + 1]) p = nxt[p];
if (s2[i] == s2[p + 1]) ++p;
nxt[i] = p;
}
KMP 自动机的状态集合 为 ,如果当前识别的字符串为 ,那么每个节点 表示已经输入的所有字符 与 的最大匹配长度为 ,即满足 ,转移函数为:
int tr[1005][26];
for (int i = 0; i <= n; ++i)
for (int j = 0; j < 26; ++j) {
if (i < n && s[i + 1] == j + 'a') tr[i][j] = i + 1;
else if (i) tr[i][j] = tr[nxt[i]][j];
}
CF1163D。找一个模式串在字符串中的出现次数,而看到可以在 * 上填任意一个字母又是经典 DP 问题。对 建出 KMP 自动机,设 考虑到 的第 位,此时在 的 KMP 自动机的节点 , 的 KMP 自动机的节点 的最大答案。采用刷表法转移,模式串 在转移到节点 时有贡献。初始 ,转移 ,目标 。代码。
Aho-Corasick 自动机
例题
字符串回文结构
Manacher
回文自动机
例题
字符串子串结构
通常来说,刻画 的子串结构的方式有:后缀数组、后缀自动机、后缀树。
神风敢死队炸毁了此处的内容。
本部分将在 NOI2024 前进行完善。
后缀数组
SA 例题
后缀自动机
SAM 是一个接受字符串 的所有后缀的最小 DFA。其满足从 到任意状态的路径与 的所有子串一一对应, 到终止状态集合 的路径与 的后缀一一对应。SAM 的有向无环转移图称为 DAWG。
下图的左侧是 abcbc
的一个 SAM:
我们先给出一些定义:
- 代表 中所有 出现的结束位置的集合。
引理 :字符串 的两个非空子串 和 的 相同,当且仅当 在 中的每次出现都是以 的后缀形式出现的。
比较显然。
引理 :如果 是 的后缀,那么 ,否则 。
也比较显然。
如果 集合相等,那么这两个子串可以被成为等价类。SAM 中的每个状态对应一个等价类。
引理 :对于一个 ,其对应的子串集合中,较短者一定是较长者的后缀。
也比较显然。
- 代表状态 可以代表的所有子串集合,
- 代表状态 所对应的最长子串;
- 代表状态 所对应的最短子串;
- 。
定义状态 的后继状态 指向 最长的一个后缀 ,满足 。所有的后缀链接形成一棵以 为根的有根树,另外有 。
之前给出的图的右侧是一棵后缀链接构成的树。可以发现,如果按照 集合构造树,那么构造出来的树是相同的。
模板,求出 endpos
大小和 len
即可,构建 SAM 的代码如下:
#define cpy(x, y, s) memcpy(x, y, sizeof(x[0]) * (s))
int cnt = 1, las = 1, son[N][S], len[N], fa[N];
void ins(char s) {
int it = s - 'a', cur = ++cnt, p = las;
len[cur] = len[p] + 1; las = cur;
while (!son[p][it]) son[p][it] = cur, p = fa[p];
if (!p) return fa[cur] = 1, void();
int q = son[p][it];
if (len[q] == len[p] + 1) return fa[cur] = q, void();
int cl = ++cnt; cpy(son[cl], son[q], S);
len[cl] = len[p] + 1, fa[cl] = fa[q], fa[q] = fa[cur] = cl;
while (son[p][it] == q) son[p][it] = cl, p = fa[p];
}
后缀树
广义 SAM
SAM 例题
使用 SAM 解决更为方便。
[SDOI2016] 生成魔咒
SAM 上新增一个状态答案会增加 ,开个 map
跑 SAM 即可。代码。
[CF235C] Cyclical Quest
将字符串破环成链,在 SAM 上跑字符串匹配,失配了就跳 即可,代码。
杂技
对称压缩 SAM
无相幽闭蒙蔽了你的双眼。
本部分将不会在 NOI2024 前更新。