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自动机复习的主要内容,如果未能解决你的问题,请参考以下文章
HDU3247 Resource Archiver(AC自动机+BFS+DP)
HDU4057 Rescue the Rabbit(AC自动机+状压DP)