AC自动机复习

Posted Harris-H

tags:

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

AC自动机复习

1.前置知识

  • k m p kmp kmp的思想
  • T r i e Trie Trie
  • 图论入门

2.导入

AC自动机用来解决多个模式串匹配文本串的问题的。但是如果把每个模式串拼在一起的话 或者每次匹配一个的话,肯定会超时的。而 A C AC AC自动机的思想就是把所有模式串建立一棵字典树 T r i e Trie Trie. 然后用文本串来进行匹配.

为了使算法的时间复杂度能更加优秀。因此我们需要找到一个方法能实现文本串能在 T r i e Trie Trie树进行快速跳转,因此 A C AC AC自动机便诞生了。

3.Fail指针

Fail指针就是当前模式串失配后下一个跳转的位置。

构建Fail指针就是为了找到最长的 与当前失配模式串的后缀 相同的 与下一个模式串的前缀。

就是因为要利用已知信息来加速匹配,那么为什么要最长呢?

其实最长的话就可以保证每个有可能匹配的模式串都匹配到,假设当前节点模式串失配了,就跳最大的前缀,如果又失配,就又到最大前缀的最大前缀,直到没有为止,这样就可以保证全部考虑到。

4.算法流程

  • 初始化一个虚根0,便于失配能跳回到正确的位置。
  • 将字符串插入到 T r i e Trie Trie中。

字符串插入到 T r i e Trie Trie中就是 T r i e Trie Trie的基本操作,一般我们都会对字符串的结尾做一个标记。

  • F a i l Fail Fail指针

因为 T r i e Trie Trie是一棵树,所以我们可以考虑根据父亲结点来更新对应的儿子结点的 F a i l Fail Fail指针。因此在这里我们采用 B F S BFS BFS广度优先搜索,一层一层的进行搜索,这样就能根据每个父亲结点来更新儿子结点了。

这里我们人为定义0号结点(也就是虚根) 是 T r i e Trie Trie的第一层。

因为我们知道 1 1 1个字母是没有后缀的,所以对于所有第二层的结点,他们的 F a i l Fail Fail指针应该指向 0 0 0号结点,然后按照 b f s bfs bfs的思想把他们都丢入队列。


接下来就是关键了,如何根据当前结点 u u u 来更新所有的儿子结点。

我们定义 s o n = t r [ u ] [ i ] son=tr[u][i] son=tr[u][i]

这里我们分两种情况讨论:

  • 当前 s o n son son 存在。
  • 当前 s o n son son不存在。

对于第一种情况:

如果当前结点 u u u s o n son son存在 则它的 f a i l fail fail指针指向(它父亲 u u u失配指针指向的 i i i儿子结点),然后 u u u s o n son son入队 。

这里是贪心的思想,显然父亲结点的 F a i l Fail Fail指针是最优的,而他们有相同的儿子,所以 s o n son son F a i l Fail Fail指针也是最优的。

对于第二种情况:

我们直接把 s o n = t r [ f a i l [ u ] ] [ i ] son=tr[fail[u]][i] son=tr[fail[u]][i]

即: u u u i i i儿子为它的父亲 u u u失配时指向结点的 i i i儿子结点.

这里可能有人会问如果 t r [ f a i l [ u ] ] [ i ] tr[fail[u]][i] tr[fail[u]][i]是空的呢,这里不会影响,因为前面我们已经把第二层的结点 F a i l Fail Fail指针指向 0 0 0 了,所以即使不存在,最终也会指向 0 0 0,从而保证了 F a i l Fail Fail指针的正确性。

5.模板代码

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=2e6+5,M=1e6+5,inf=0x3f3f3f3f,mod=1e9+7;
#define mst(a) memset(a,0,sizeof a)
#define lx x<<1
#define rx x<<1|1
#define reg register
#define PII pair<int,int>
#define fi first 
#define se second
int tri[N][26],fail[N],cnt[N],id;
void Insert(string s){	//构建字典树. 将模式串插入字典树. 
	int rt=0;
	for(int i=0;i<s.size();i++){
		 int j=s[i]-'a';
		if(!tri[rt][j])	//如果rt没有j儿子,则新增结点. 
			tri[rt][j]=++id;
		rt=tri[rt][j];//rt变为它的j儿子. 
	}
	cnt[rt]++;//记录模式串的个数. 
}
void getFail(){	//求失配指针fail 
	queue<int>q;
	for(int i=0;i<26;i++)	//显然第二层结点没有失配后缀 所以直接指向0 
		if(tri[0][i]) fail[tri[0][i]]=0,q.push(tri[0][i]);	//第二层即所有模式串的首字母丢进队列. 
	while(!q.empty()){
		int u=q.front();q.pop();
	for(int i=0;i<26;i++){	//如果当前结点u的i儿子存在 则它的fail指针指向(它父亲u失配指针指向的i儿子结点),然后u的i儿子入队 
		 if(tri[u][i]) fail[tri[u][i]]=tri[fail[u]][i],q.push(tri[u][i]);
		 else tri[u][i]=tri[fail[u]][i];
	}		//否则u的i儿子为它的父亲u失配时指向结点的i儿子结点. 
	} 
}
int query(string s){	//查询模式串在文本串出现过的次数. 
	int u=0,ans=0;
	for(int i=0;i<s.size();i++){
		u=tri[u][s[i]-'a'];
		for(int j=u;j&&~cnt[j];j=fail[j])	//进行匹配直到已经匹配过或者匹配到根. 
			ans+=cnt[j],cnt[j]=-1;
	}
	return ans;
}
int main(){
	int n;
	string s;
	scanf("%d",&n);
	for(int i=0;i<n;i++)
		cin>>s,Insert(s);
	getFail();
	cin>>s;
	printf("%d\\n",query(s)); 
	return 0;
}

6.习题

传送门:

https://harris.blog.csdn.net/article/details/107282362

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

POJ2778DNA Sequence(AC自动机)

HDU3247 Resource Archiver(AC自动机+BFS+DP)

POJ3691DNA repair(AC自动机,DP)

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

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

BZOJbzoj1030[JSOI2007]文本生成器(AC自动机)