AC自动机
引入
什么是自动机?
OI中所说的“自动机”一般都指“确定有限状态自动机”。自动机是OI、计算机科学中被广泛使用的一个数学模型,其思想在许多字符串算法中都有涉及,因此推荐在学习一些字符串算法(KMP、AC 自动机、SAM)前先完成自动机的学习。学习自动机有助于理解上述算法。
好的 其实上面并没有什么软用
AC自动机(Aho-Corasick automaton) 可以大概理解为是在Trie数上跑KMP 用于解决单文本多模式串匹配问题
KMP与Trie树
这边就不再赘述 若不会请读者自行学习 KMP学习 Tire树学习
基本的KMP 这边直接放代码 对着读一下应该没太大问题
#include<string.h>
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<vector>
#include<map>
#include<set>
using namespace std;
typedef long long LL ;
typedef pair<LL,LL> pii ;
#define fre(x) freopen(#x".in","r",stdin) ; freopen(#x".out","w",stdout) ;
#define re register
#define temp template<typename T>
temp
inline T read (T x = 0 , T f = 0){
re char ch = getchar() ;
for (;!isdigit(ch);ch = getchar() ) if (ch == '-') f = 1 ;
for (;isdigit(ch);ch = getchar()) x = (x<<1) + (x<<3) + (ch^48) ;
return f? ~x+1 : x ;
}
const LL N = 1e6+5 ;
string s1 , s2 ;
LL len1 , len2 ;
LL Next[N] ;
vector<LL> ans ;
inline void KMP(){
LL k = 0 ;
for(LL i = 1 ; i <= len1 ; ++i ){
while(k && s1[i] != s2[k+1] ) k = Next[k] ;
if(s2[k+1] == s1[i] ) ++k ;
if(k == len2 ){
ans.push_back(i - k);
k = Next[k] ;
}
}
return ;
}
inline void Getnext(){
Next[0] = 0 ;
LL k = 0 ;
for(LL i = 2 ; i <= len2 ; ++i ){
while(k && s2[k+1] != s2[i] ) k = Next[k] ;
if(s2[k+1] == s2[i]) ++k ;
Next[i] = k ;
}
return ;
}
signed main(){
cin >> s1 >> s2 ;
s1 = ' ' + s1 ;
s2 = ' ' + s2 ;
len1 = s1.length() - 1 ;
len2 = s2.length() - 1 ;
Getnext() ;
KMP() ;
if(ans.size())
for(LL i:ans){
printf("%lld\n",i+1) ;
}
for(LL i = 1 ; i <= len2 ; ++i){
printf("%lld ",Next[i]<0?0:Next[i]) ;
}
return 0;
}
昂……Trie树的话 Acwing前缀统计[Trie树模板]
代码也放在这里了
#include <bits/stdc++.h>
using namespace std ;
int read(int x = 0,bool f = false,char ch = getchar()) {
for (;!isdigit(ch);ch = getchar()) f |= ch == '-' ;
for (;isdigit(ch);ch = getchar()) x = (x << 1) + (x << 3) + (ch ^ 48) ; return f ? ~ x + 1 : x ;
}
class Trie {
private :
int Tp , value_type ;
Trie *son[26] ;
public :
Trie(int x):Tp(x){
for (int i = 0 ; i < 26 ; ++i ) son[i] = NULL ;
}
void insert(Trie* &Root,char str[]) {
int len = strlen(str) ;
Trie* p = Root ;
for (int i = 0 ; i < len ; ++i ) {
int alpha = str[i] - 'a' ;
if (p -> son[alpha] == NULL) {
p -> son[alpha] = new Trie(0) ;
p -> son[alpha] -> value_type = alpha ;
}
p = p -> son[alpha] ;
}
++ p -> Tp ;
}
int search(Trie* &Root,char str[]) {
int len = strlen(str) , ans = 0 ;
Trie* p = Root ;
for (int i = 0 ; i < len ; ++i ) {
int alpha = str[i] - 'a' ;
if (p -> son[alpha] == NULL) return ans ;
p = p -> son[alpha] ;
ans += p -> Tp ;
} return ans ;
}
} ;
const int N = 1e6 + 5 ;
int n , m ;
char str[N] ;
signed main() {
Trie* Answer = new Trie(0) ;
n = read() ; m = read() ;
for (int i = 1 ; i <= n ; ++i )
scanf("%s",str) , Answer -> insert(Answer,str) ;
for (int i = 1 ; i <= m ; ++i )
scanf("%s",str) , printf("%d\n",Answer -> search(Answer,str)) ;
return 0 ;
}
正题AC自动机
Build构建
-
建立一个Trie树 并把模式串插入
-
为每一个点建立一个失配指针Fail(PS:失配 即在此处匹配失效 下面会具体讲到)
Fail失配指针
定义
一个状态\(u\)指向另一个状态\(v\) 其中\(v \in Trie\) 求v为u的最长后缀(即若干个后缀状态取一个最长的)
就大概类比一下KMP的Next指针
但也稍有不同
-
共同点 两者都是失配后的跳转指针
-
不同点 Next求最长的Border 而Fail指向的是所有状态的前缀指向当前状态的最长后缀
没看懂 那看看我从Oi-wiki上扒下来的图
对于现在有一个u状态 它的父亲节点为fa fa通过了alpha字符边指向了u 也就是\(Trie_{fa,alpha} = u\) 并且假设深度小于u的节点的Fail指针都已经求得
-
\(Trie[fail[fa],alpha]\)存在 那么我们直接让\(fail[u]\)指向这玩意
-
\(Trie[fail[fa],alpha]\)不存在 那咋滴子办吗 这不是显然吗 直接找\(Trie[fail[fail[fa]],alpha]\)存不存在 再一直跳上去 跳到根节点 不过到哪里都没有 那就真没办法了 指向根节点吧
那么如何做到深度小的都先求出来呢 —— BFS! 因为Trie是一颗树 阿Sir
下面是代码实现
void spawn_fail(Aho_Corasick_automaton* &Rt) {
queue<Aho_Corasick_automaton*> Q ;
Aho_Corasick_automaton* u ;
for(int i = 0; i < 30; i++) {
if(Rt -> son[i] != NULL)
Rt -> son[i] -> nxt = Rt , Q.push(Rt -> son[i]) ;
else
Rt -> son[i] = Rt ;
}
while(Q.size()) {
u = Q.front() ; Q.pop() ;
for (int i = 0 ; i < 30 ; ++i )
if (u -> son[i] == NULL) {
u -> son[i] = u -> nxt -> son[i] ;
} else {
Q.push(u -> son[i]) ;
u -> son[i] -> nxt = u -> nxt -> son[i] ;
}
}
}
别问变量为什么那么长 因为有 自动补全 真心建议用Visual Studio或者Visual Studio Code欸嘿
询问Query
这个跟KMP真的差不多
把文本串在Trie树上扫一遍 再扫的途中失配或者匹配完了通过\(Fail\)跳转一下就行了
在匹配字符串的过程中,我们会舍弃部分前缀达到最低限度的匹配。\(Fail\)指针则指向了更多的匹配状态。
我的记录方法有亿些奇怪 由于用指针写 访问下标不要太麻烦
于是乎 直接把这个节点的权值(即是模式串尾的个数)赋值为\(-1\)
因为不可能有\(-1\)个串经过这里吧…… 注意如果这样写 根节点也需要赋成\(-1\)
贴上我的代码
void Find(Aho_Corasick_automaton* &Rt,char str[]) {
ans = 0 ; Aho_Corasick_automaton* o = Rt ;
len = strlen(str) ;
for (int i = 0 ; i < len ; ++i ) {
alpha = str[i] - 'a' ;
Aho_Corasick_automaton* p = o -> son[alpha] ;
while (p -> Tp != -1) {
ans += p -> Tp ;
p -> Tp = -1 ;
p = p -> nxt ;
} o = o -> son[alpha] ;
}
}
例题
综上所述 直接贴代码了
#include <bits/stdc++.h>
using namespace std ;
int read(int x = 0,bool f = false,char ch = getchar()) {
for (;!isdigit(ch);ch = getchar()) f |= ch == '-' ;
for (;isdigit(ch);ch = getchar()) x = (x << 1) + (x << 3) + (ch ^ 48) ;
return f ? ~ x + 1 : x ;
}
const int N = 1e5 + 5 , L = 1e6 + 5 ;
int tmp , cnt = 1 , ans , n , len , alpha ;
class Aho_Corasick_automaton {
private :
int Tp , idx , value_type ;
Aho_Corasick_automaton *son[30] , *nxt ;
public :
Aho_Corasick_automaton (int x):Tp(x) {
value_type = 0 , idx = ++ cnt , nxt = NULL ;
for (int i = 0 ; i < 30 ; ++i )
son[i] = NULL ;
}
void Insert (Aho_Corasick_automaton* &Rt,char str[]) {
Aho_Corasick_automaton* o = Rt ;
len = strlen(str) ;
for (int i = 0 ; i < len ; ++i ) {
alpha = str[i] - 'a' ;
if (o -> son[alpha] == NULL) o -> son[alpha] = new Aho_Corasick_automaton(0) , o -> son[alpha] -> value_type = alpha ;
o = o -> son[alpha] ;
}
++ o -> Tp ;
}
void spawn_fail(Aho_Corasick_automaton* &Rt) {
queue<Aho_Corasick_automaton*> Q ;
Aho_Corasick_automaton* u ;
for(int i = 0; i < 30; i++) {
if(Rt -> son[i] != NULL)
Rt -> son[i] -> nxt = Rt , Q.push(Rt -> son[i]) ;
else
Rt -> son[i] = Rt ;
}
while(Q.size()) {
u = Q.front() ; Q.pop() ;
for (int i = 0 ; i < 30 ; ++i )
if (u -> son[i] == NULL) {
u -> son[i] = u -> nxt -> son[i] ;
} else {
Q.push(u -> son[i]) ;
u -> son[i] -> nxt = u -> nxt -> son[i] ;
}
}
}
void Find(Aho_Corasick_automaton* &Rt,char str[]) {
ans = 0 ; Aho_Corasick_automaton* o = Rt ;
len = strlen(str) ;
for (int i = 0 ; i < len ; ++i ) {
alpha = str[i] - 'a' ;
Aho_Corasick_automaton* p = o -> son[alpha] ;
while (p -> Tp != -1) {
ans += p -> Tp ;
p -> Tp = -1 ;
p = p -> nxt ;
} o = o -> son[alpha] ;
}
}
} ;
int T ;
char str[L] , substr[L] ;
signed main() {
n = read() ; Aho_Corasick_automaton* Answer = new Aho_Corasick_automaton(-1) ;
for (int i = 1 ; i <= n ; ++i ) scanf("%s",str) , Answer -> Insert(Answer,str) ;
scanf("%s",substr) ; Answer -> spawn_fail(Answer) ;
Answer -> Find(Answer,substr) ;
printf("%d\n",ans) ;
}
LOJ上还有一系列的AC自动机的好题
而且还有一定的优化或者改善 能够帮助大家理解AC自动机
本文来自博客园,作者:xxcxu,转载请注明原文链接:https://www.cnblogs.com/Maraschino/articles/15168780.html