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;
		}
	}
}
原文地址:https://www.cnblogs.com/zYzYzYzYz/p/14448348.html