AC自动机

Posted 。✧* ꧁王者꧂✧*

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了AC自动机相关的知识,希望对你有一定的参考价值。

今日份的博客来袭。
AC自动机,很疑问它为什么要叫这个名字,但这并不是重点(我管它什么名字呢~)。
曾经学kmp的时候就听教练提过一嘴,说AC自动机怎么样怎么样,今天终于见到了!(结果是我被无尽的泪水淹没了┭┮﹏┭┮)
在这里插入图片描述
AC自动机说难也难,因为它的本质不是特别容易理解。
但它说不难也不难,因为你只要会用就行了。
在这里插入图片描述
但是,相比于一知半解,还是啃透比较好吧。
学AC自动机需要两个前置技能: K M P KMP KMP t r i e trie trie树。
但是,个人觉得这两者中,先学会 K M P KMP KMP比较重要,因为AC自动机最难理解的东西其实学 K M P KMP KMP时已经学过了,就是那个 n e x t next next数组。
K M P KMP KMP中,字符串 A A A n e x t [ i ] next[i] next[i]的含义是: A A A中以 i i i结尾非前缀子串所能匹配的 A A A的最大前缀。是不是挺绕的,下面我们就来画一波图:
在这里插入图片描述

如图,对于 a b c a b c a abcabca abcabca,因为 n e x t next next数组表示的是非前缀子串与前缀子串匹配, a b c abc abc找不到一个非本身后缀与非本身前缀对应,所以它们的 n e x t next next值就为 0 0 0,而 a b c a abca abca中的第一个 a a a与最后一个 a a a可以匹配,所以 n e x t [ 4 ] = 1 next[4]=1 next[4]=1,而 a b c a b c a abcabca abcabca中后缀 a b c a abca abca与前缀 a b c a abca abca匹配,所以 n e x t [ 7 ] = 4 next[7]=4 next[7]=4
而AC自动机也有一个类似的 n e x t next next数组,但其并不是在线性结构上搞,而是将其存进一个类似于 t r i e trie trie树的结构。这个结构长这样:
就比如,我们把 a b c abc abc a a a aaa aaa存进这个结构:
在这里插入图片描述
画得不好看。。。
也就是说,第一层对应的是所有字符串中第一位的字符,对于重复出现的,我们只存一个点,第二层,第三层也是如此。
然后,这个 n e x t next next数组,抽象点说,对应的不只是本串的最大前缀了,而是整个AC自动机所存的最大前缀。上图:
在这里插入图片描述

我们将 a a b c aabc aabc a a b aab aab以及 a b c abc abc存进了AC自动机里去,对于 a a b c aabc aabc,其后缀的最大前缀对应了 a b c abc abc
a a b aab aab的后缀对应了 a b ab ab,所以, n e x t next next数组实际上也就在AC自动机的图上加上了对应关系。
附上一段建图的代码:

#include<bits/stdc++.h>//存进n个字符串
using namespace std;
const int N=1e4+10;
const int S=26;
int nex[N*S],q[N*S],tr[N*S][S],n,idx,cnt[N*S];
char s[1000010];
void insert()//插入
{
	int p=0;
	for(int i=1;s[i];i++)
	{
		int j=s[i]-'a';
		if(!tr[p][j]) tr[p][j]=++idx;//当前节点下没有要存的点,再开一个 
		p=tr[p][j];
	}
	cnt[p]++;//某一个字符串已经存完 
}
void build()//建next数组
{
	int l=1,r=0;
	for(int i=0;i<26;i++)
	if(tr[0][i]) q[++r]=tr[0][i];//bfs 
	while(l<=r)
	{
		int t=q[l++];
		for(int i=0;i<S;i++)
		{
			if(!tr[t][i]) continue;
			int c=tr[t][i];
			int j=nex[t];
			while(j&&!tr[j][i]) j=nex[j];//找nex 
			if(tr[j][i]) j=tr[j][i];
			nex[c]=j;
			q[++r]=c;
		}
	}
}
int main()
{
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		scanf("%s",s+1);
		insert();
	}
	build();
	return 0;
}

注:上述代码只负责建图。
这里还有一段 f a i l fail fail优化后的建图代码(对应的还有 f a i l fail fail树这一结构,本次先不讲):

#include<bits/stdc++.h>
using namespace std;
const int N=1e4+10,S=26;
int tr[N*S][S],cnt[N*S],idx,n,q[N*S],nex[N*S];
char s[1000001];
void insert()
{
	int p=0;
	for(int i=1;s[i];i++)
	{
		int t=s[i]-'a';
		if(!tr[p][t]) tr[p][t]=++idx;
		p=tr[p][t];
	}
	cnt[p]++;
}
void build()
{
	int l=1,r=0;
	for(int i=0;i<S;i++)
	if(tr[0][i]) q[++r]=tr[0][i];
	while(l<=r)
	{
		int o=q[l++];
		for(int i=0;i<S;i++)
		{
			int c=tr[o][i];
			if(!c)
			tr[o][i]=tr[nex[o]][i];//!!注意,这里不一样。 
			else
			{
				nex[c]=tr[nex[o]][i];
				q[++r]=c;
			}
		}
	}
}
int main()
{
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		scanf("%s",s+1);
		insert();
	}
	build();
	scanf("%s",s+1);
	return 0;
}

两处代码不一样的地方就在于 n e x t next next数组找相同前缀时的向前跳跃,优化代码是为了跳一次就找到可以停下来的节点。优化思想:将处理过的信息存下,当前向下找若没有对应的点,便存上一个点的信息,这样,每次指针向上跳跃只用跳一次就足够了。
AC自动机是多模匹配算法,用于处理多个字符串的信息。
emmmm。。。博客更新结束。

以上是关于AC自动机的主要内容,如果未能解决你的问题,请参考以下文章

POJ3691DNA repair(AC自动机,DP)

HDU4057 Rescue the Rabbit(AC自动机+状压DP)

Codeforces 86C Genetic engineering(AC自动机+DP)

POJ1699 Best Sequence(AC自动机+状压DP)

POJ - 2778 ~ HDU - 2243 AC自动机+矩阵快速幂

HDU2457 DNA repair(AC自动机+DP)