AC自动机总结
AC自动机结合了Trie的结构和KMP失配的思想,解决的是多模式串匹配问题。
步骤:
-
将所有模式串构成Trie
-
对每个Trie上的节点构造失配指针\(fail\)。
之后就可以利用构建出的AC自动机解决各种问题。
这里的Trie的节点表示一种状态,状态是某个模式串的前缀(即从Trie上的节点走到根经过的前缀),边表示转移,即转移到在当前节点的状态下加上某个字符后的状态。记所有状态的集合为\(Q\)。
\(fail\)的构建
对于状态\(u\),\(fail_u=v\),使得\(v\in Q\)且\(v\)是\(u\)的最长后缀。
考虑对于当前节点\(u\),设其父亲为\(p\),\(p\)通过字符\(c\)的边指向\(u\)即\(tr[p,c]=u\)。
我们假设现在深度小于\(u\)的节点的\(fail\)都已经求得。(深度小于\(u\)即状态的长度小于\(u\),这是真正要限定的。)
-
若\(tr[fail_p,c]\)存在,那么\(fail_u=tr[fail_p,c]\)。即在\(p\)后加上\(c\)的最长后缀就是在\(fail_p\)后加上\(c\)。
-
若\(tr[fail_p,c]\)不存在,就继续跳\(fail\)。
-
若\(fail\)跳到根之后也没找到,那么\(fail_u=0\)
以上只是构建\(fail\)的基本思想。实际运用时,我们要对\(fail\)做路径压缩,即若\(tr[u,c]\)不存在,那么\(tr[u,c]=tr[fail_u,c]\)。这样修改Trie的结构后,我们在跳边的时候可能会丢失前缀,但没关系,后缀能匹配那么原串就一定能匹配,并且\(fail\)其实也是在丢失前缀。
拓扑优化
对于\(fail\)连成的边,有一个性质:在AC自动机中只保留\(fail\)边,一定构成树。这是因为\(fail\)一定不成环,且深度比当前点小。
我们在统计答案时会沿着\(fail\)不断跳,那么原问题就变成了树上的链求和问题,可以拓扑排序优化建图,最后统计答案时按拓扑序递推过去。(不用真建\(fail\)树,只需记录入度。)
实现:
void ins(char s[],int idx){
int u=0;
for(int i=0;s[i];++i){
if(!tr[u][s[i]-'a']) tr[u][s[i]-'a']=++sz;
u=tr[u][s[i]-'a'];
}
if(!val[u]) val[u]=idx;
rev[idx]=val[u];//或者换成其他操作
}
void build(){
queue<int> q;
for(int i=0;i<26;++i){
if(tr[0][i]) ind[0]++,q.push(tr[0][i]);
}
while(!q.empty()){
int u=q.front();
q.pop();
for(int i=0;i<26;++i){
if(tr[u][i]) fail[tr[u][i]]=tr[fail[u]][i],ind[tr[fail[u]][i]]++,q.push(tr[u][i]);
else tr[u][i]=tr[fail[u]][i];
}
}
}
void qry(char s[]){
int u=0;
for(int i=0;s[i];++i){
u=tr[u][s[i]-'a'];
cnt[u]++;//或者换成其他操作
}
}
void topu(){
queue<int> q;
for(int i=0;i<=sz;++i){
if(!ind[i]) q.push(i);
}
while(!q.empty()){
int u=q.front();
q.pop();
ans[val[u]]=cnt[u];
int v=fail[u];
cnt[v]+=cnt[u];//或者换成其他递推操作
if(!--ind[v]) q.push(v);
}
}

浙公网安备 33010602011771号