AC自动机
AC自动机
define
tire[i].son[j]
表示第 i 个节点的字母 j 儿子 。
tire[i].End
表示这个节点是几个字符串的最后一个字母。
tire[i].next
失配指针。
insert()
void insert()
{
int now=0;//当前节点
for(int i=1;i<=Len;i++){
int ch=str[i]-'a';
if(!tire[now].son[ch])tire[now].son[ch]=++num;//如果没出现过就新建一个
now=tire[now].son[ch];
}
tire[now].End++;//节点now作为了一个字符串的终止节点
}
插入很简单。
处理失配指针
next 是找到一个最长的后缀是自己的前缀,这个后缀不能是自己本身。
考虑 kmp 是怎么求 next 的。
while(j&&B[i]!=B[j+1])j=nxt[j];
这个 \(i\) 和 \(j\) (\(j\) 比 \(i\) 短) 同时指向 2 个前缀,并且 \(j\) 是 \(i-1\)的前缀。
如果 \(j+1\) 不等于 \(i\) 那么令 \(j=nxt[j]\) ,就是说如何 \(j+1\) 接不上 \(i\) 那么考虑 \(j\) 的前缀 \(j’\) (\(j’\) 是 \(j\) 的后缀同时也是 \(i-1\) 的后缀)。
那么我们也可以在 tire 树上这么求。
首先如何保证 j 比 i 短呢?
可以在 tire 树上面 bfs 。
然后枚举每个点 x 的 son[i]。
- 如果它有 son[i] ,那么让你儿子 \(i\) 的失配指针指向你的失配指针的儿子 \(i'\) ,因为你的前缀加一个字母 i 肯定是你加一个字母 i 形成的字符串的前缀。
- 否则直接让 son[i] 指向 tire[nxt].son[i] ,和 kmp 那个同理就是说如果 \(j+1\) 接不上 \(i\) 那么考虑 \(j\) 的前缀 \(j'\)。
这里有一个疑问,好像缺了个 while ,如果 \(j’\) 也接不上呢?
这是一个优化,这里当然可以写一个 while 循环一步一步向上跳,但是常数有点大。
假设写 while 时会沿着 A->B->C->D 跳。
先假设之前的 next 和 son[i] 都是对的。
- 再假设这一条路都接不上字母 i,那么 B,C,D 都会指向 0,然后令 \(A.son[i]=B.son[i]\) 这肯定也是 0,是对的。
- 再假设 C,D 能接上,那么 B 自然指向 C ,然后令 \(A.son[i]=B.son[i]\) 这肯定也是 C,是对的。(因为C有儿子 i,那么它指向自己的儿子)
这样就能一步到位。
void prepare()
{
tou=1;wei=0;
for(int i=0;i<26;i++)
if(tire[0].son[i])
que[++wei]=tire[0].son[i];//先把长度为1的入队。
while(tou<=wei){
int x=que[tou++];
for(int i=0;i<26;i++)
if(tire[x].son[i]){
tire[tire[x].son[i]].next=tire[tire[x].next].son[i];
que[++wei]=tire[x].son[i];
}else tire[x].son[i]=tire[tire[x].next].son[i];
}
}
计算有多少个T出现在S中
先把所有的 T 插入 tire树 中,模仿插入的方法,拿 S 在 tire树 上跑一圈(这一圈肯定不会加入新节点,因为对于每个节点,就算后面接不上字母i,那么它的儿子都指向了他的失配节点的儿子)。
如果跑到一个地方,发现 tire[x].End
不为-1,那么就加上他的贡献,把 tire[x].End
改成 -1,紧接着沿着它的失配指针查跑到节点 0,同时做同样的操作。(如果向上跳的过程中发现 tire[y].End
是 -1,那么就break,因为沿着它的失配指针到达的节点 \(z\) 都被经历过了)。
void calc()
{
int now=0;
for(int i=1;i<=Len;i++){
int ch=str[i]-'a';
now=tire[now].son[ch];
for(int t=now;t&&tire[t].End!=-1;t=tire[t].next){
ans+=tire[t].End;
tire[t].End=-1;
}
}
}