AC 自动机 - OI Wiki (oi-wiki.org)
给定一个长度为m主串再给出n个平均长度为w模式串问这些模式串分别出现了多少次。
如果对n个模式串分别进行kmp算法那么时间复杂度:n次匹配 每次(m+w) 所以是O(nm+nw)
ac自动机时间复杂度: 建树O(w*n) 建立fail数组 O(w*n) 匹配O(w*m) 所以是O(wm+nw)
所以可知当n相对于w很大即模式串的数量较模式串的平均长度很大时就应该用AC自动机来解决
对于字典树我们知道,字典树是用主串建树,然后对于各个模式串分别在主串上面跑一遍,而ac自动机是对于模式串建树,然后把主串在模式串上面跑一遍。
AC自动机算法分为3步:构造一棵Trie树,构造失败指针和模式匹配过程。
如果你对KMP算法和了解的话,应该知道KMP算法中的next函数(shift函数或者fail函数)是干什么用的。KMP中我们用两个指针i和j分别表示,A[i-j+ 1..i]与B[1..j]完全相等。也就是说,i是不断增加的,随着i的增加j相应地变化,且j满足以A[i]结尾的长度为j的字符串正好匹配B串的前 j个字符,当A[i+1]≠B[j+1],KMP的策略是调整j的位置(减小j值)使得A[i-j+1..i]与B[1..j]保持匹配且新的B[j+1]恰好与A[i+1]匹配,而next函数恰恰记录了这个j应该调整到的位置。同样AC自动机的失败指针具有同样的功能,也就是说当我们的模式串在Tire上进行匹配时,如果与当前节点的关键字不能继续匹配的时候,就应该去当前节点的失败指针所指向的节点继续进行匹配。
举例:
以要查找的单词建树:say he shr her
初始化
const int N = 1e6 + 6;
int n;
char s[N];
namespace AC {
int tr[N][26], tot;
int e[N], fail[N];
建树
void insert(char *s) {
int u = 0;
for (int i = 1; s[i]; i++) {
if (!tr[u][s[i] - 'a']) tr[u][s[i] - 'a'] = ++tot; //如果没有则插入新节点
u = tr[u][s[i] - 'a']; //搜索下一个节点
}
e[u]++; //尾为节点 u 的串的个数
}
构造失败指针
queueq;
void build() {
for (int i = 0; i < 26; i++)
if (tr[0][i]) q.push(tr[0][i]);
while (q.size()) {
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]; // fail数组:同一字符可以匹配的其他位置 这里的tr[fail[u][i]]就是所求的最长后缀
q.push(tr[u][i]);
} else
tr[u][i] = tr[fail[u]][i];
}
}
}
fail[i]=j 说明以i为终止节点的单词的最长后缀是以j为终止节点的单词,所以fail[u]表示当前主串匹配到了u这个位置,那么fail[u]这个位置也一定匹配成功了,如匹配到了she的e,那么he也一定匹配成功了
匹配
这里 作为字典树上当前匹配到的结点,res
即返回的答案。循环遍历匹配串, 在字典树上跟踪当前字符。利用 fail 指针找出所有匹配的模式串,累加到答案中。然后清零。在上文中我们分析过,字典树的结构其实就是一个 trans 函数,而构建好这个函数后,在匹配字符串的过程中,我们会舍弃部分前缀达到最低限度的匹配。fail 指针则指向了更多的匹配状态。最后上一份图。对于刚才的自动机:
int query(char *t) {
int u = 0, res = 0;
for (int i = 1; t[i]; i++) {
u = tr[u][t[i] - 'a']; // 转移
for (int j = u; j && e[j] != -1; j = fail[j]) {
res += e[j], e[j] = -1;
}
}
return res;
}
}
hdu3065 链接
#include
using namespace std;
const int maxn=2e5+5;
int u=0;
int tree[maxn][26];
int fail[maxn];
int e[maxn];
mapmp;
mapcheck;
vectorvec;
void build(char s[]){
int len=strlen(s);
int h=0;
for(int i=0;iq;
void build_fail(){
for(int i=0;i<26;i++){
if(tree[0][i]){
q.push(tree[0][i]);
}
}
while(q.size()){
int now=q.front();
q.pop();
for(int i=0;i<26;i++){
if(tree[now][i]){
fail[tree[now][i]]=tree[fail[now]][i];
q.push(tree[now][i]);
}
else{
tree[now][i]=tree[fail[now]][i];
}
}
}
}
void find(string s){
int u=0;int res=0;
for(int i=0;i>n;
while(n--){
char s[55];
cin>>s;
vec.push_back(s);
build(s);
}
build_fail();
string tmp;cin>>tmp;
find(tmp);
for(int i=0;i
AC自动机的应用
例题
https://www.acwing.com/problem/content/submission/code_detail/15098046/
对于各个治病DNA片段可以当作是模式串,对于模式串建立ac自动机,然后对于主串进行修改,看最少要修改多少个位置可以(让主串与trie树没有匹配使得到了u这个模式的时候,u的后缀含有治病DNA片段),dp解决
#include
#define io ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
using namespace std;
const int maxn=4e3+5;
const int inf=1e9+7;
int n;
int tot;
int cnt=0;
int tree[maxn][4];
int fail[maxn];
int st[maxn];
mapmp;
mapmpp;
string s;
int check[maxn][1005];
void insert(string s){
int u=0;
for(int i=0;iq;
for(int i=0;i<4;i++){
if(tree[u][i]){
q.push(tree[u][i]);
}
}
while(q.size()){
int now=q.front();
q.pop();
for(int i=0;i<4;i++){
if(tree[now][i]){
fail[tree[now][i]]=tree[fail[now]][i];
if(st[fail[tree[now][i]]]){
st[tree[now][i]]=1;
}
q.push(tree[now][i]);
}
else{
tree[now][i]=tree[fail[now]][i];
}
}
}
}
int dp(int u,int len){
if(len==s.size()){
return check[u][len]=0;
}
if(check[u][len]!=-1){
return check[u][len];
}
int res=inf;
for(int i=0;i<4;i++){
if(!st[tree[u][i]]){
res=min(res,dp(tree[u][i],len+1)+(mpp[i]!=s[len]));
}
}
return check[u][len]=res;
}
void solve(){
memset(tree,0,sizeof(tree));
memset(st,0,sizeof(st));
memset(check,-1,sizeof(check));
memset(fail,0,sizeof(fail));
cnt=0;
while(n--){
cin>>s;
insert(s);
}
build_fail();
cin>>s;
int ans=dp(0,0);
if(ans>=1e9){
ans=-1;
}
cout<<"Case "<>n){
if(n==0){
break;
}
tot++;
solve();
}
}