AC自动机
AC自动机
AC自动机适用于多个字符串的匹配,以及一系列引申出来的问题
Trie图
通过将Trie树转化为Trie图,并且使这些非树边到达的地方是“所有模式串的前缀中匹配当前状态的最长后缀。”
上图!(灰色的边:字典树的边,黑色的边:AC 自动机修改字典树结构连出的边。)
例如,当前的状态是\(hers\),而下一个字母是\(h\),而在原本的Trie树上是没有边的,也就是失配了,而在Trie图上,我们转移到了\(hersh\)的在“模式串的前缀中匹配当前状态的最长后缀”,即\(sh\),从而能尽可能的匹配到模式串
fail数组
和Trie的非树边类似,fail数组指向当前状态\(S\)的“所有模式串的前缀中匹配当前状态的最长后缀。”
同样是上面的图(黄色的边:fail 指针)
fail\([she]\)便指向\(he\)
fail的指向还会构成一颗树,满足任意一节点\(S\)的祖先节点全都是\(S\)的后缀
构建
按Trie树的深度(字符串的长度)从小到大处理(bfs)
设当前状态是\(sh\),我们从\(a\sim z\)枚举,如果有在Trie树的边(\(tr[sh][e]\)),就更新\(fail[tr[sh][e]]\),否则则修改非树边\(tr[sh][i]\)
那用什么更新呢?;
以下用\(x\)表示当前节点,\(son\)表示下一个节点,\(son=x+s\),即当前边表示\(s\)
想一下fail的定义,“所有模式串的前缀中匹配当前状态的最长后缀。”,一个一个去找肯定不可能,那能不能利用已经求来的呢?
答案是肯定的,而且就是运用\(x\)的信息,\(fail[x]\)是匹配\(x\)的最长后缀,如果\(tr[fail[x]][s]\)是原本Trie树上的边,那\(fail[son]=tr[fail[x]][s]\)
那如果不是呢,也没有关系,此时\(tr[fail[x]][s]\)就表示匹配\(fail[x]\)的最长后缀,也就是匹配\(x\)的最长后缀,也就是\(fail[son]=tr[fail[x]][s]\)
同样,更新非树边也是这个道理
代码:
void add(string a){
m=a.size();
int p=0;
for(int i=0;i<m;i++){
if(tr[p][a[i]-'a'])p=tr[p][a[i]-'a'];
else{
top++;
tr[p][a[i]-'a']=top;
p=top;
}
}
en[p]++;
}
void build(){
queue<int> q;
for(int i=0;i<=25;i++){
if(tr[0][i])q.push(tr[0][i]);
}
while(q.size()){
int x=q.front();q.pop();
for(int i=0;i<=25;i++){
if(tr[x][i]){
fail[tr[x][i]]=tr[fail[x]][i];
q.push(tr[x][i]);
}
else tr[x][i]=tr[fail[x]][i];
}
}
}
运用
- 基础板子
- DP
- fail树
总结
AC自动机实际上就是对Trie树进行一些处理,使来处理的匹配串能在失配的情况下,迅速跳到最能匹配的位置,即“所有模式串的前缀中匹配当前状态的最长后缀。”
同时,fail数组则可以维护当前状态的最长的为Trie节点的后缀,通过fail树的操作还可以处理所有为Trie节点的后缀