前置知识:AC自动机
后缀自动机(SAM)
引入
给你一个字符串,要求给出一个点数,边数线性的自动机,满足这个自动机能表示出 s 的所有子串。
一个想法是将 s 的所有后缀取出,然后加入一个 AC 自动机中,这样显然行不通,但是如果将某些性质相同的点看作同一个点,那么就可能建出要求的自动机。
定义
如图,有 1 个起点 S,每 1 条边代表1个字符,从 S 出发的所有路径(指蓝边)都对应了原串的 1 个子串
所有蓝边构成1个有向无环图(拓扑图),所有绿边构成1棵树
性质
定义节点i对应的字符串为自动机中从节点S出发到i的最长路径
endpos(i) 表示为节点i对应字符串在原串中匹配成功时的所有末尾下标集合
当 endpos(i)=endpos(j) 时,我们称 i,j 属于同一等价类。
则有以下性质:
-
令 s1,s2 为 S 的两个子串 ,不妨设 |s1|≤|s2| (我们用|s|表示 s 的长度 ,此处等价于s1 不长于s2)。则s1 是 s2 的后缀当且仅当 endpos(s1)⊇endpos(s2) ,s1 不是 s2 的后缀当且仅当 endpos(s1)∩endpos(s2)=∅ 。
-
两个不同子串的endpos,要么有包含关系,要么没有交集。
-
两个子串的endpos相同,那么短串为长串的后缀。
-
对于一个等价类 ,已知两个子串 i,j 属于这个等价类且 |i|<|t|<|j|,其中 t 为 j 的某个后缀,那么 t 也一定属于这个等价类。
-
对于一个等价类,它包含的所有串的一定是最长串的一段连续后缀,例如最长串为 ciallo,那么这个等价类可以为 {ciallo,iallo,allo}。
后缀Link
对于一个等价类 v,设 w 表示其中的最短串,则 v 的后缀 link 指向 w 的长度为 |w|−1 的后缀 所在的等价类。
构造
点的定义:属于同一等价类的子串集被称为一个点。
边的定义:枚举所有非空子串 b,设 a 为 b 去掉末尾字符 c 的字符串,则连一条边从 a 所在的等价类到 b 所在的等价类,边权 c,忽略重边和自环。
采用数学归纳法,我们假设我们已经拥有一个前 n−1 个字符组成字符串 a 的 sam,现在新加入一个字符 c,观察 sam 发生的改变。
对于边来说,我们只对所有以下标 n−1 结尾的子串加边到一个新节点 c,也就是只更新串 a 所在的等价类 v,link(v),link(link(v)) 等等。
对于点来说,可能某些串 a 和串 b 原本属于同一等价类,但加入 c 后,a 多了一个 endpos 为 n,于是 a,b 分裂。
那么 a 一定是 v,link(v),link(link(v)),… 中某个向权值为 c 的边走的点。
具体如何构造就看代码,注意:
对于节点i,有len,fa,ch[],
len 表示为从节点S出发到i的最长路径长度,
fa 表示为endpos包含i,且endpos与i最接近的点,即绿边,
ch[] 表示为从i出发的边,即蓝边。
void extend(int c)
{
int p = last, np = last = ++ tot;
node[np].len = node[p].len + 1;
for (; p && !node[p].ch[c]; p = node[p].fa) node[p].ch[c] = np; // 加边
if (!p) node[np].fa = 1;
else
{
int q = node[p].ch[c];
if (node[q].len == node[p].len + 1) node[np].fa = q;
else
{
int nq = ++ tot; // 开始分裂
node[nq] = node[q], node[nq].len = node[p].len + 1;
node[q].fa = node[np].fa = nq;
for (; p && node[p].ch[c] == q; p = node[p].fa) node[p].ch[c] = nq;
}
}
}
广义后缀自动机
定义
对于多个串建广义自动机,自动机内包含每个串的子串。
构造
最容易想到的是将小串合并为大串,S=S1+#
+S2+$
+S3+⋯,坏处是没有那么多特殊字符,强行使用需要加特判,所以不常使用。
将所有串建一个 trie 树,然后 bfs 整棵树(注意 dfs 是假的),每次加入时 last 设置为父节点的 np。
代码
#include <bits/stdc++.h>
using namespace std;
const int N = 2000010;
int tot0 = 1, tot1 = 1;
int trie[N][26], pos[N];
struct Node {
int ch[26], len, fa;
} node[N];
void ins(string t) {
int p = 1;
for (int i = 0; i < t.size(); i ++ ) {
int c = t[i] - 'a';
if (!trie[p][c]) trie[p][c] = ++ tot0;
p = trie[p][c];
}
}
int extend(int c, int lst) {
int p = lst, np = lst = ++ tot1;
node[np].len = node[p].len + 1;
for (; p && !node[p].ch[c]; p = node[p].fa) node[p].ch[c] = np;
if (!p) node[np].fa = 1;
else {
int q = node[p].ch[c];
if (node[q].len == node[p].len + 1) node[np].fa = q;
else {
int nq = ++ tot1;
node[nq] = node[q], node[nq].len = node[p].len + 1;
node[q].fa = node[np].fa = nq;
for (; p && node[p].ch[c] == q; p = node[p].fa) node[p].ch[c] = nq;
}
}
return np;
}
void build() {
queue<int> Q;
pos[1] = 1;
for (int i = 0; i < 26; i ++ )
if (trie[1][i])
Q.push(trie[1][i]), pos[trie[1][i]] = extend(i, 1);
while (Q.size()) {
int t = Q.front();
Q.pop();
for (int i = 0; i < 26; i ++ )
if (trie[t][i])
Q.push(trie[t][i]), pos[trie[t][i]] = extend(i, pos[t]);
}
}
int main() {
int T;
scanf("%d", &T);
while (T -- ) {
string s;
cin >> s;
ins(s);
}
build();
long long ans = 0;
for (int i = 2; i <= tot1; i ++ )
ans += node[i].len - node[node[i].fa].len;
printf("%lld\n%d\n", ans, tot1);
return 0;
}
upd on 2024.8.31