只是给忘记模板时的我看的
AC自动机
大概流程
首先对所有模式串建出 T r i e Trie Trie 树,并标记。
f a i l fail fail 的定义:设 i i i 节点所代表的字符串为 S S S,则 f a i l i fail_i faili 表示 S S S 的所有后缀里面,在 T r i e Trie Trie 树中出现过的最长的那个串所代表的节点。
然后通过 bfs \texttt{bfs} bfs 求出 f a i l fail fail,代码如下:
void getfail()
{
queue<int>q;
for(int i=0;i<26;i++)
if(t[0].ch[i])
q.push(t[0].ch[i]);
while(!q.empty())
{
int u=q.front();
q.pop();
for(int i=0;i<26;i++)
{
if(t[u].ch[i])
{
t[t[u].ch[i]].fail=t[t[u].fail].ch[i];
q.push(t[u].ch[i]);
}
else t[u].ch[i]=t[t[u].fail].ch[i];//tag1
}
}
}
其中为什么 t a g 1 tag_1 tag1 处可以将 u u u 的儿子直接指向 f a i l u fail_u failu 的儿子 v v v:
首先实际的操作应该是新建一个虚拟节点 n e w new new,使 n e w new new 为 u u u 的儿子,且 f a i l n e w = v fail_{new}=v failnew=v。
又由于 n e w new new 本身是新建的节点,没有任何儿子,所以 n e w new new 的儿子全都是要靠新建虚拟节点构成。
所以 n e w new new 的子树其实和 v v v 的子树是一模一样的。
那我们不妨用同一棵子树表示他们,也就是说让 u u u 的儿子指向 v v v 而不是新建节点。
然后由于 n e w new new 树的 f a i l fail fail 全部都是指向 v v v 树的,所以合并到一起不会对 f a i l fail fail 产生影响。
那么 getfail ( ) \operatorname{getfail}() getfail() 之后原来的 T r i e Trie Trie 树就会变成一个 DAG 了。
实际应用
一、模式串与文本串匹配上的应用
原理
首先通过递归 f a i l fail fail,就可以遍历某个串的所有在模式串中出现过的后缀。
同样,如果建立 f a i l fail fail 树( f a i l i → i fail_i\to i faili→i),就可以通过遍历某一个点 u u u 的子树(设 u u u 所代表的串为 s s s),遍历所有以 s s s 为后缀的串。(也就是往 s s s 的前面加字符)
其次,对于原 T r i e Trie Trie 树中的某一个节点 u u u(设其代表的串为 s s s),可以遍历统计 u u u 子树内的所有点,遍历所有以 s s s 为前缀的串。(也就是往 u u u 后面加字符)
那么综合上面两个操作,对于某个串 t t t,我们可以求出所有满足 t t t 是 s s s 的子串的 s s s 串的信息。
时间复杂度为 O ( n ) O(n) O(n)(遍历一遍 T r i e Trie Trie 树+一遍 f a i l fail fail 树)。
所以对于解决模式串类的问题,AC 自动机的本质就是对于每一种字符串,除了记录在它后面加字符能到达的出现过的串( T r i e Trie Trie 树),还记录了在它前面加字符能到达的出现过的串( f a i l fail fail 树)。
那么对于 s s s 串的子串信息,我们可以对 s s s 的前缀跳 f a i l fail fail 链。而对于 t t t 串的扩展串信息( t t t 是某个串的子串),我们可以在 f a i l fail fail 树中遍历 t t t 树的子树,再在 T r i e Trie Trie 树中遍历 遍历到的点 的子树。
例题
1.请你分别求出每个模式串 T i T_i Ti 在文本串 S S S 中出现的次数。
可以直接按我们刚刚的做法来做(跳 S S S 前缀的 f a i l fail fail 链),但是会 T 飞。
考虑优化,把根到 S S S 路径上的节点都标记(设为 s i z e = 1 size=1 size=1),然后建立 f a i l fail fail 树( f a i l i → i fail_i \to i faili→i),设 s i z e i size_i sizei 为 i i i 这个节点所代表的字符串在 S S S 中出现的次数。
那么在 f a i l fail fail 树中, i i i 的子树中的所有有效节点都能为 s i z e i size_i sizei 贡献 1 1 1。所以把每一个有效节点 s i z e size size 的初始值都设为 1 1 1 然后在 f a i l fail fail 树上从下往上统计 s i z e size size。
#include<bits/stdc++.h>
#define N 200010
#define ll long long
using namespace std;
struct Trie
{
int ch[26],fail;
ll size;
}t[N];
int n,node,id[N];
int cnt,head[N],nxt[N],to[N];
void adde(int u,int v)
{
to[++cnt]=v;
nxt[cnt]=head[u];
head[u]=cnt;
}
int insert(string s)
{
int u=0,len=s.size();
for(int i=0;i<len;i++)
{
int v=s[i]-'a';
if(!t[u].ch[v]) t[u].ch[v]=++node;
u=t[u].ch[v];
}
return u;
}
void dfsTrie(string s)
{
int u=0,len=s.size();
for(int i=0;i<len;i++)
{
int v=s[i]-'a';
u=t[u].ch[v];//这里可能没有u->v这个转移然后回到根,但也是对的。因为这代表在Trie树中没有出现任何一个s[1...i]的后缀(注意这里的转移时geifail后的)
t[u].size++;
}
}
void getfail()
{
queue<int>q;
for(int i=0;i<26;i++)
if(t[0].ch[i])
q.push(t[0].ch[i]);
while(!q.empty())
{
int u=q.front();
q.pop();
for(int i=0;i<26;i++)
{
if(t[u].ch[i])
{
t[t[u].ch[i]].fail=t[t[u].fail].ch[i];
q.push(t[u].ch[i]);
}
else t[u].ch[i]=t[t[u].fail].ch[i];
}
}
for(int i=1;i<=node;i++)
adde(t[i].fail,i);
}
void dfsFail(int u)
{
for(int i=head[u];i;i=nxt[i])
{
int v=to[i];
dfsFail(v);
t[u].size+=t[v].size;
}
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
string str;
cin>>str;
id[i]=insert(str);
}
getfail();
string s;
cin>>s;
dfsTrie(s);
dfsFail(0);
for(int i=1;i<=n;i++)
printf("%lld\n",t[id[i]].size);
return 0;
}
/*
3
abc
cde
de
abcde
*/
2.https://blog.youkuaiyun.com/ez_lcw/article/details/99613063
后缀自动机(SAM)
大概流程
(以下的 “节点” 均表示后缀自动机中的节点)
(定义对于两个字符串 A , B A,B A,B 的运算 A + B A+B A+B 表示 A A A 和 B B B 顺次拼接起来的串)
(下面请注意 S ( i ) S(i) S(i) 和 S [ i ] S[i] S[i] 的区别,其中后者表示字符串 S S S 的第 i i i 位,而前者在下文中会有定义)
Endpos \operatorname{Endpos} Endpos 集合
我们把 S S S 的一个子串在 S S S 中每一次出现的结束位置的集合定义为 Endpos \operatorname{Endpos} Endpos 集合。
然后我们考虑我们要构建的后缀自动机长什么样:我们将 Endpos \operatorname{Endpos} Endpos 集合完全相同的子串合并到同一个节点。
我们发现,对于越短的子串,其 Endpos \operatorname{Endpos} Endpos 集合往往越大。更具体地,如果 t t t 是某一个子串 T T T 的后缀,则 ∣ Endpos ( t ) ∣ ≥ ∣ Endpos ( T ) ∣ |\operatorname{Endpos}(t)|\geq |\operatorname{Endpos}(T)| ∣Endpos(t)∣≥∣Endpos(T)∣。当且仅当取等号时, t t t 和 T T T 会被压缩到同一个节点中。
而对于某一个子串 T T T 来说,肯定有一个分界长度 l e n len len,使得每一个长度 ≥ l e n \geq len ≥len 的 T T T 的后缀的 Endpos \operatorname{Endpos} Endpos 都和 Endpos ( T ) \operatorname{Endpos}(T) Endpos(T) 相同(所以这些后缀和 T T T 在同一个节点),且每一个长度 < l e n <len <len 的 T T T 的后缀的 Endpos \operatorname{Endpos} Endpos 大小都比 Endpos ( T ) \operatorname{Endpos}(T) Endpos(T) 大(所以这些后缀和 T T T 不在同一个节点,而且这些后缀可能在不同的节点)。
所以每个节点 u u u 中存储的一定是一堆长度连续的子串,且短的串是长的串的后缀。不妨把这些串的集合称为 S ( u ) S(u) S(u),设其中最长的串为 longest ( u ) \operatorname{longest}(u) longest(u),最短的串为 shortest ( u ) \operatorname{shortest}(u) shortest(u)。
我们在具体实现时会用一个 l e n len len 数组记录每个节点中最长的子串的长度(即 longest ( u ) \operatorname{longest}(u) longest(u) 的长度),为什么不用记最短的长度,下文会讲。
Parent Tree
如上文所述,对于每一个子串都会有唯一一个 ”分界长度“,而且每一个节点中所有子串的 “分界长度” 都相同,为这个节点中最短的子串的长度。
而如果 t t t 是 T T T 的一个后缀且没有和 T T T 分在一个节点中,那么 t t t 肯定也是别的子串的后缀,例如 ab \texttt{ab} ab 在串 cabzab \texttt{cabzab} cabzab 中既可以是 cab \texttt{cab} cab 的后缀,也可以是 zab \texttt{zab} zab 的后缀。这样我们看到:长的串 T T T 只能 “对应” 唯一的一个短的串 t t t,而短的串可以 “对应” 多个长的串,如果将 “短的串” 视为 “长的串” 的父亲,这就构成了一棵严格的树形结构。我们称为Parent Tree。
形式化地说,对于一个节点 u u u,我们找到 S ( u ) S(u) S(u) 中某一个子串 T T T 的后缀 t t t,使得 t t t 不在 S ( u ) \operatorname{S}(u) S(u) 中且满足 ∣ t ∣ |t| ∣t∣ 最大(显然 t t t 是 S ( u ) S(u) S(u) 中任何一个串的后缀且 ∣ t ∣ |t| ∣t∣ 等于 S ( u ) S(u) S(u) 中任何一个串的 “分界长度“ 减 1 1 1),记 u u u 的后缀链接 link ( u ) \operatorname{link}(u) link(u) 为 t t t 所属的节点。那么 link \operatorname{link} link 所构成的就是这个 Parent Tree。
这时你会发现 shortest ( u ) \operatorname{shortest}(u) shortest(u) 的长度其实就是 longest ( link ( u ) ) \operatorname{longest}(\operatorname{link}(u)) longest(link(u)) 的长度加 1 1 1,即 l e n ( link ( u ) ) + 1 len(\operatorname{link}(u))+1 len(link(u))+1,所以我们无需记录 shortest ( u ) \operatorname{shortest}(u) shortest(u) 的长度。
SAM 的转移
对于一个节点 u u u,在 S ( u ) S(u) S(u) 中的某一个串后面添加一个字符 c c c 变成一个新的串,如果这个新的串仍是 S S S 的子串(那么由于 S ( u ) S(u) S(u) 中的任意一个串在 S S S 的某个位置出现, S ( u ) S(u) S(u) 中的其他串肯定也会在同样位置出现,所以此时 S ( u ) S(u) S(u) 中的所有串添加这个字符 c c c 所形成的的新串也都仍是 S S S 的子串 ( 1 ) {\,}^{(1)} (1)),设这个新串所属的节点为 p p p,那么我们记录转移 c h [ u ] [ c ] ← p ch[u][c]\gets p ch[u][c]←p。
注意对于添加字符 c c c 而言,添加 c c c 后的新串可能不同,但它们的 Endpos \operatorname{Endpos} Endpos 都是相同的,因为新串中的某一个串在某个位置出现,那么将它末尾的 c c c 删除后, S ( u ) S(u) S(u) 中的其他串也肯定会在同样位置出现,然后再加上末尾的 c c c,于是所有的新串也都会在同样的位置出现。这同时说明了 c h [ u ] [ c ] ch[u][c] ch[u][c] 是唯一的 ( 2 ) {\,}^{(2)} (2)。
但注意这些新串所属的等价类 S ( c h [ u ] [ c ] ) S(ch[u][c]) S(ch[u][c]) 不一定只包含这些新串 ( 3 ) {\,}^{(3)} (3)。同时也说明了有可能有多个 c h [ u ] [ c ] ch[u][c] ch[u][c] 指向同一个点,于是 SAM 实际上是一个 DAG。
算法(实现)
考虑从前往后加入 S S S 的每一个字符,假设当前加入的是 c = S [ x ] c=S[x] c=S[x]。
加入字符 c c c 的实际操作是把 S [ 1.. x ] S[1..x] S[1..x] 的所有后缀的 Endpos \operatorname{Endpos} Endpos 集合都改变了(新增加了元素 x x x),考虑这将如何影响后缀树的形态,那么我们先要找到 S [ 1.. x ] S[1..x] S[1..x] 的所有后缀所在的节点。
那我们肯定要先新建一个节点 n o w now now 表示 S [ 1.. x ] S[1..x] S[1..x] 的 Endpos \operatorname{Endpos} Endpos 等价类,因为这个等价类之前一直没有出现过。
我们上一次插入 S [ x − 1 ] S[x-1] S[x−1] 的时候肯定也新建了一个节点表示 S [ 1.. x − 1 ] S[1..x-1] S[1..x−1] 的 Endpos \operatorname{Endpos} Endpos 等价类,记这个节点为 l a s t last last。
根据 ( 1 ) (1) (1),由于 S [ 1.. x ] S[1..x] S[1..x] 是 S [ 1.. x − 1 ] S[1..x-1] S[1..x−1] 末尾添加字符 c c c 后得到的串,那么 S ( l a s t ) + c S(last)+c S(last)+c 的所有串都应该属于同一个 Endpos \operatorname{Endpos} Endpos 等价类,于是直接 c h [ l a s t ] [ c ] ← n o w ch[last][c]\gets now ch[last][c]←now。
接着,令 p = l a s t p=last p=last,然后让 p p p 沿着 link \operatorname{link} link 往上跳,并且一直记录 c h [ p ] [ c ] ← n o w ch[p][c]\gets now ch[p][c]←now,直到满足已经存在转移 c h [ p ] [ c ] ch[p][c] ch[p][c] 了(此时证明 S [ 1.. x − 1 ] S[1..x-1] S[1..x−1] 中出现过 S [ 1.. x ] S[1..x] S[1..x] 的后缀)。
让 p p p 一直往上跳的过程实际上相当于从长到短枚举 S [ 1.. x − 1 ] S[1..x-1] S[1..x−1] 后缀中的每一种 Endpos \operatorname{Endpos} Endpos 定价类,也就相当于把 S [ 1.. x − 1 ] S[1..x-1] S[1..x−1] 的所有后缀都枚举一遍,而判断是否已经存在转移 c h [ p ] [ c ] ch[p][c] ch[p][c] 也就相当于把 S [ 1.. x ] S[1..x] S[1..x] 的每一个后缀都枚举了一遍(因为满足一个串 T T T 是 S [ 1.. x ] S[1..x] S[1..x] 的后缀的必要条件是 T T T 去掉最后一位后是 S [ 1.. x − 1 ] S[1..x-1] S[1..x−1] 的后缀),并判断它们是否在 S [ 1.. x − 1 ] S[1..x-1] S[1..x−1] 中出现过。
所以如果跳到某个 p p p 仍然不存在转移 c h [ p ] [ c ] ch[p][c] ch[p][c],即 S ( p ) + c S(p)+c S(p)+c(显然这是 S [ 1.. x ] S[1..x] S[1..x] 的一段长度连续的后缀)没有在 S [ 1.. x − 1 ] S[1..x-1] S[1..x−1] 中出现过,那么 S ( p ) + c S(p)+c S(p)+c 的 Endpos \operatorname{Endpos} Endpos 集合和 S [ 1.. x ] S[1..x] S[1..x] 的是一样的,即 S ( p ) + c S(p)+c S(p)+c 包含于 S ( n o w ) S(now) S(now),于是我们直接令 c h [ p ] [ c ] ← n o w ch[p][c]\gets now ch[p][c]←now,再继续往上跳即可。
接下来我们分情况讨论:
-
如果就这么顺着 Parent Tree 跳一直跳到了根节点还要往上,此时证明 S [ 1.. x ] S[1..x] S[1..x] 的任何一个后缀都没有在 S [ 1.. x − 1 ] S[1..x-1] S[1..x−1] 中出现过,那么我们直接让 link ( n o w ) = r t \operatorname{link}(now)=rt link(now)=rt 即可。
-
否则,如果我们在跳的过程中找到了一个 p p p 使得已经存在转移 c h [ p ] [ c ] ch[p][c] ch[p][c] 了,我们就先设 q = c h [ p ] [ c ] q=ch[p][c] q=ch[p][c]。
但注意此时仅满足 S ( p ) + c S(p)+c S(p)+c 包含于 S ( q ) S(q) S(q),所以并不一定是 S ( q ) S(q) S(q) 中所有串的 Endpos \operatorname{Endpos} Endpos 集合都改变了,即 S ( q ) S(q) S(q) 里面不一定全是 S [ 1.. x ] S[1..x] S[1..x] 的后缀。
可以发现 S ( q ) S(q) S(q) 中所有 longest ( p ) + c \operatorname{longest}(p)+c longest(p)+c 的后缀(即 S ( q ) S(q) S(q) 中所有长度小于等于 l e n ( p ) + 1 len(p)+1 len(p)+1 的串)都是 S [ 1.. x ] S[1..x] S[1..x] 的后缀(尽管这些串中可能有长度短的一部分并不属于 S ( p ) + c S(p)+c S(p)+c,但他们仍然是 S [ 1.. x ] S[1..x] S[1..x] 的后缀,我们一起考虑),它们的 Endpos \operatorname{Endpos} Endpos 集合都改变了。
同时 S ( q ) S(q) S(q) 中所有长度大于 l e n ( p ) + 1 len(p)+1 len(p)+1 的串都一定不是 S [ 1.. x ] S[1..x] S[1..x] 的后缀(因为这个 p p p 使我们最先找到的,即 longest ( p ) + c \operatorname{longest}(p)+c longest(p)+c 应该是 S [ 1.. x ] S[1..x] S[1..x] 在 S [ 1.. x − 1 ] S[1..x-1] S[1..x−1] 中出现的最长的后缀),它们的 Endpos \operatorname{Endpos} Endpos 集合都没有改变。
然后我们再分情况讨论:
-
若 l e n ( q ) = l e n ( p ) + 1 len(q)=len(p)+1 len(q)=len(p)+1,我们直接令 link ( n o w ) ← q \operatorname{link}(now)\gets q link(now)←q 即可,上面已经证明了这样的 q q q 一定是最长的。
-
若 l e n ( q ) ≠ l e n ( p ) + 1 len(q)\neq len(p)+1 len(q)=len(p)+1,此时 longest ( q ) \operatorname{longest}(q) longest(q) 不是 S [ 1.. x ] S[1..x] S[1..x] 的后缀,而且 longest ( q ) \operatorname{longest}(q) longest(q) 会比 longest ( p ) \operatorname{longest}(p) longest(p) 长一截。
那么此时 S ( q ) S(q) S(q) 中长度大于 l e n ( p ) + 1 len(p)+1 len(p)+1 和长度小于等于 l e n ( p ) + 1 len(p)+1 len(p)+1 的两部分串的 Endpos \operatorname{Endpos} Endpos 集合已经不同了,需要分离。
于是我们新建一个点 n q nq nq,表示 S ( q ) S(q) S(q) 中长度小于等于 l e n ( p ) + 1 len(p)+1 len(p)+1 的那一部分串的 Endpos \operatorname{Endpos} Endpos 等价类。这样就在 q q q 和 f = link(q) f=\operatorname{link(q)} f=link(q) 之间新插入了一个点,所以 link ( q ) ← n q \operatorname{link}(q)\gets nq link(q)←nq, link ( n q ) ← f \operatorname{link}(nq)\gets f link(nq)←f。同时更新 l e n ( n q ) ← l e n ( p ) + 1 len(nq)\gets len(p)+1 len(nq)←len(p)+1。也要更新 c h [ n q ] ← c h [ q ] ch[nq]\gets ch[q] ch[nq]←ch[q](更新 c h [ n q ] ← c h [ q ] ch[nq]\gets ch[q] ch[nq]←ch[q] 的原因上面 ( 1 ) (1) (1) 处有提到)。
同时要让 link ( n o w ) ← n q \operatorname{link}(now)\gets nq link(now)←nq,上面同样也已经证明了这样找到的 n q nq nq 一定是最长的。
最后,我们就要更新我们还要继续让 p p p 沿着 link \operatorname{link} link 往上跳,如果 c h [ p ] [ c ] = q ch[p][c]=q ch[p][c]=q,那么更新 c h [ p ] [ c ] ← n q ch[p][c]\gets nq ch[p][c]←nq(这里这么更新的证明比较显然,略去),否则停止上跳退出。
然后就结束了吗? q q q( n q nq nq)在 Parent Tree 上的祖先(即 p p p 在 Parent Tree 上的祖先往 c c c 的转移)的 Endpos \operatorname{Endpos} Endpos 集合都有改变,它们不需要更新吗?事实上由于这些点所包含的所有串的 Endpos \operatorname{Endpos} Endpos 集合都同样增加了一个元素 x x x(而且由于增加的元素为 x x x,所以这些点的转移不可能有更新),于是经过若干推导可知 SAM 的结构并没有改变,所以我们无需更新。
-
这样 SAM 就建好了。
实际应用
咕咕咕……
后缀树
定义
后缀树定义比 SAM 简单很多。对于串 S S S 的后缀树,我们先把串 S S S 的所有后缀各加入一个终止符后都插入到一棵 Trie 树中,比如对于串 banana \texttt{banana} banana,将得到下面这么一棵 Trie 树:(图来自于 EA’s blog)
但这样节点数是 O ( n 2 ) O(n^2) O(n2) 的,但我们发现这棵 Trie 树上有很多节点只有一个儿子,这样构成了若干条单链,我们可以把这些链进行压缩,变成这样:
这样压缩后的字典树我们就把它称为后缀树。
这样的后缀树的节点数量是 O ( n ) O(n) O(n) 级别的,因为它只有 n n n 个叶子(终止符),而且每个点的儿子个数都大于 1 1 1,于是就能用类似虚树的方式证明出这棵树的节点至多只有 2 n − 1 2n-1 2n−1 个。
再根据等一下会说的结论,这也侧面证明了 SAM 的节点个数至多为 2 n − 1 2n-1 2n−1 个。
构建
直接构建后缀树有 Ukkonen 算法,但是实际上我们可以用 SAM 来构建。
结论:串 S S S 在 SAM 上的 Parent Tree 为串 S S S 的反串的后缀树。
假设现在有某个串 S ′ S' S′,我们先定义 S ′ S' S′ 的某个子串在 S ′ S' S′ 中出现的所有位置的左端点集合为 leftpos \operatorname{leftpos} leftpos 集合。这个定义和 Endpos \operatorname{Endpos} Endpos 类似。
然后你发现后缀树上的一条边就代表着一个 leftpos \operatorname{leftpos} leftpos 等价类,因为这条边上的所有点都没有分支,意味着对于这条边上的任意两个长度相差 1 1 1 串 A , A + c A,A+c A,A+c, A A A 只会出现在 A + c A+c A+c 中,否则若 A A A 还出现在 A + c ′ A+c' A+c′ 中那么就会有 c ′ c' c′ 这个分支,就矛盾了。
于是后缀树上的一个点 u u u 就能代表它往父亲的那条边的 leftpos \operatorname{leftpos} leftpos 等价类,于是可以类似地定义 longest ′ ( u ) \operatorname{longest}'(u) longest′(u) 表示 u u u 所代表的 leftpos \operatorname{leftpos} leftpos 等价类中的所有串中最长的那个,显然 u u u 中的其他串都是 longest ′ ( u ) \operatorname{longest}'(u) longest′(u) 的前缀。
而且对于后缀树上点 u u u 的父亲 f f f,肯定有 longest ′ ( f ) \operatorname{longest}'(f) longest′(f) 是 longest ′ ( u ) \operatorname{longest}'(u) longest′(u) 的所有前缀中和 longest ′ ( u ) \operatorname{longest}'(u) longest′(u) 不属于同一个 leftpos \operatorname{leftpos} leftpos 集合的最长的前缀。
发现这和 SAM 的 Parent Tree 类似,于是把 S S S 反串, leftpos \operatorname{leftpos} leftpos 变为 Endpos \operatorname{Endpos} Endpos,就可以得到上面的结论了。