前言
AC 自动机用于处理多模式串匹配长字符串问题。
前置知识:Trie KMP
实现
About fail
先给所有模式串建一个 Trie,然后使用一个 fail 指针。
fail[u] 指向结点 uuu 所代表的字符串的最长在 Trie 上存在的后缀。
假如 uuu 的父亲是 ppp,并且 (u,p)(u, p)(u,p) 表示字符 ccc,那么如果 fail[p] 有 ccc 这个儿子,那么 fail[u] = t[fail[p]][c],否则看 fail[fail[p]] 有没有 ccc 这个儿子,直到 p=0p = 0p=0,不存在后缀,那么 fail[u]=fail[0]=0fail[u] = fail[0] = 0fail[u]=fail[0]=0。
注意,由于 fail 指针是在比 uuu 深度小的字符串上乱跳的,所以要 bfs 处理。
void build(){
queue<int> q;
for(int i = 0;i < 26;i++)
if(tr[0][i])
q.push(tr[0][i]);
while(!q.empty()){
int i = q.top(); q.pop();
for(int i = 0;i < 26;i++){
if(tr[u][i]){
fail[tr[u][i]] = tr[fail[u]][i];
q.push(tr[u][i]);
}
else
tr[u][i] = tr[fail[u]][i];
}
}
}
代码里面有一个很妙的点,就是这句:
else
tr[u][i] = tr[fail[u]][i];
我们发现,之前如果在跳 fail 指针的时候继续跳的条件就是不存在 iii 儿子。那么我在不存在的儿子上面直接先跳,相当于一个路径压缩,可达大大提高效率。
About query
关于 AC 自动机是如何匹配的。
核心思想是每次找儿子,如果没有儿子的话就跳 fail 指针,直到有儿子。
P3808
先看例题:假如我们想知道现在有几个模式串在长字符串里。
int query(string str){
int len = str.size();
int u = 0, ans = 0;
for(auto c : str){
u = tr[u][c - 'a'];// 指向结点的 c 儿子
for(int v = u;v && cnt[v] != -1;v = fail[v]){// 一直找 fail 指针,直到找到之前找过的路。
// 容易发现只有当找到(表示模式串的结点)的时候才有贡献,所以其他时候 ans += cnt[v] 这句话是没用的
ans += cnt[v];
cnt[v] = -1;
}
}
}
我们知道假儿子实际上指向的是 fail 指针,所以 u = tr[u][c - 'a'] 相当于跳了 fail 指针。
然后我们要找这个字符串表示的后缀中有多少个模式串,继续跳 fail,直到找过了。
P3796
再看例题:我们要求在长字符串中出现次数最多的若干个模式串。
void query(string str){
int u = 0;
for(auto c : str){
u = t[u][c - 'a'];// 指向结点的 c 儿子
for(int v = u; v; v = fail[v]){// 一直找 fail 指针。
// 容易发现只有当找到(表示模式串的结点)的时候才有贡献,所以其他时候这句话是没用的
ans[f[v]].first++;
}
}
}
其中,f[i] 表示在字典树中以 iii 结尾的结点所表示的模式串编号。若 iii 不表示模式串结尾,那么 fi=0f_{i}= 0fi=0,这个时候,ans[f[v]] 无论怎么加,对答案都没有贡献。
关于 AC 自动机的套路
Fail 树
在 AC 自动机上面,满足一个奇妙的性质:fail 指针构成了一棵树。
假如我们需要统计每个模式串在长字符串中出现的次数,用上面的做法会 TLE。
我们知道,上面的做法实际上是将这个点到根的 fail 链上的所有结点都 +1+1+1,如果结点刚好表示一个模式串,那么对答案的共享就 +1+1+1。
实际上,我们只要在结点上打标机,表示从结点到跟的所有经过的点都 +1+1+1。
最后,再进行一次树形 dp 即可。
su=∑v∈son(u)sv s_{u} = \sum\limits_{v \in son(u)} s_v su=v∈son(u)∑sv
例题 P5357,统计每个模式串在长字符串中出现的次数。
思路如上,没有细节。
589

被折叠的 条评论
为什么被折叠?



