AC自动机学习笔记
AC自动机学习笔记
暑训过程中重新学了一遍,简要记录一下随想。
概要
用途
主要用来解决 单词集 \(T\) 与 给定文本串 \(S\) 匹配的问题。
建树复杂度 \(O(\sum |T_i|)\),单词询问复杂度 \(O(|S| + \sum |T_i|)\)。
Trie 与 Fail
AC 自动机主要干的事就一件:建两棵树。分别是 Trie 和 Fail,其中后者是在 Trie 的基础上建立的,且为 ACAM 算法的最精髓所在。
注意理解自动机思想和动态规划思想在建树过程中的应用。
定义不多谈论,大概记录一下比较重要的性质。
1
Fail 树是一棵有根树,并且对于该树上的一对节点 \((x,v),v\in subtree(x)\),\(x\) 对应的单词是 \(v\) 对应的单词在 单词集 \(T\) 中的后缀。
2
对于 Fail 树上的一个节点 \(x\),从 \(x\) 到根节点 \(0\) 上所有节点对应单词 \(T_i\) 组成的集合,是单词集中 \(x\) 节点对应的单词的 所有后缀。
3
如果每个单词的结尾处对应的 Trie 上节点有权值 \(1\),其余节点权值为 \(0\),那么对于 Fail 树上的一个节点 \(x\),其对应单词的出现次数就是在 Fail 树中以 \(x\) 为根节点的权值和。
根据这个性质,可以完成十分经典的 ACAM 应用:分别数出多个模式串 \(T_i\) 在文本串 \(S\) 中出现的次数。
Code
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10;
int tr[N][26],fail[N],cnt;
int idx[N],ans[N];
inline void insert(string &s,int id)
{
int p=0;
for(char c:s)
{
if(!tr[p][c-'a'])tr[p][c-'a']=++cnt;
p=tr[p][c-'a'];
}
idx[id]=p;
}
int in[N];
inline void build()
{
queue<int> q;
for(int i=0;i<26;++i)if(tr[0][i])q.push(tr[0][i]);
while(q.size())
{
int x=q.front();q.pop();
for(int i=0;i<26;++i)
{
if(tr[x][i])fail[tr[x][i]]=tr[fail[x]][i],q.push(tr[x][i]),in[tr[fail[x]][i]]++;
else tr[x][i]=tr[fail[x]][i];
}
}
}
inline void query(string &s)
{
int p=0;
for(char c:s)
{
p=tr[p][c-'a'];
ans[p]++;
}
}
inline void bfs()
{
queue<int> q;
for(int i=1;i<=cnt;++i)if(!in[i])q.push(i);
while(q.size())
{
int x=q.front();q.pop();
int v=fail[x];
ans[v]+=ans[x];
if(!(--in[v]))q.push(v);
}
}
inline void solve()
{
int n;
cin>>n;
string s,t;
for(int i=1;i<=n;++i)
{
cin>>t;
insert(t,i);
}
build();
cin>>s;
query(s);
bfs();
for(int i=1;i<=n;++i)cout<<ans[idx[i]]<<'\n';
}
int main()
{
ios::sync_with_stdio(0);cin.tie(0);
return solve(),0;
}
ACAM与动态规划
//todo here
ACAM与矩阵乘法递推
//todo here
为什么要练,为什么要写?
引用一句让我幡然悔悟的话:
“练了不一定写的出来正解,不练一定写不出来正解”
本文来自博客园,作者:Hanggoash,转载请注明原文链接:https://www.cnblogs.com/Hanggoash/p/19058006

浙公网安备 33010602011771号