AC自动机

Posted wonder-land

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了AC自动机相关的知识,希望对你有一定的参考价值。

前言

在学习AC自动机前,请确保已经学习并能熟练运用:

  • KMP匹配

  • 字典树

引入

在漫长的OI路途,我们难免要接触到一种叫字符串的东西。

在解决关于字符串的问题时,我们又难免要解决两个字符串匹配的问题,

比如,在一个字符串s中,字符串t出现了多少次 这些问题。(详见KMP匹配)

而在OI路途也不是一直可以一帆风顺的,万恶的出题人绝对不会总让你只匹配一个字符串,他们肯定要想方设法提高些难度,比如一个文本串匹配多个匹配串……

显然,这时候我们不可能对着那么多个字符串一个一个的做KMP,这时候就要用到本文的内容:AC自动机

AC自动机

AC自动机是建立在字典树和KMP思想上的产物,从某种意义上来说,KMP匹配也是AC自动机的一种特殊情况(字典树只有一条链的时候)。

它通过建立所有匹配串的字典树,在每个节点建立失配指针fail来将各节点联系起来。

指针fail指向的是当前串最长后缀的节点,这样可以保证沿着fail一直跳下去,不会漏掉任何一个后缀节点。

在文本串匹配时,对于文本串的每一个前缀串,通过跳fail指针将其后缀串全部搜一遍,累加答案即可。


P3808 【模板】AC 自动机(简单版)

code:

#include<bits/stdc++.h>
using namespace std;

const int maxn=1e6+5,maxs=1e6+5;

int n,f[maxs][27],root=0,cnt=0;
int add[maxs],fail[maxs];
string s;

void insert(string s) 
	int now=root;
	for(int i=0;i<s.size();i++) 
		int c=s[i]-\'a\';
		if(!f[now][c]) f[now][c]=++cnt;
		now=f[now][c];
	
	add[now]++;
	return ;


void build() 
	queue<int>q;
	for(int i=0;i<26;i++)
		if(f[root][i]) q.push(f[root][i]);
	while(!q.empty()) 
		int now=q.front();
		q.pop();
		for(int i=0;i<26;i++)
			if(f[now][i]) 
				fail[f[now][i]]=f[fail[now]][i];
				q.push(f[now][i]);
			
			else f[now][i]=f[fail[now]][i];
	
	return ;


int query(string s) 
	int now=root,re=0;
	for(int i=0;i<s.size();i++) 
		now=f[now][s[i]-\'a\'];
		for(int j=now;j&&add[j]!=-1;j=fail[j])
			re+=add[j],add[j]=-1;
	
	return re;


int main() 
	cin>>n;
	for(int i=1;i<=n;i++) 
		cin>>s;
		insert(s);
	
	build();
	cin>>s;
	cout<<query(s);
	return 0;


P5357 【模板】AC 自动机(二次加强版)

code:

#include<bits/stdc++.h>
using namespace std;

const int maxn=2e5+5,maxs=2e5+5;

int n,ch[maxs][26],cnt=0,root=0,num[maxs],fail[maxs],idx[maxn];
string s[maxn],t;
vector<int>edge[maxs];

void insert(string s,int id) 
	int now=root;
	for(int i=0;i<s.size();i++) 
		int c=s[i]-\'a\';
		if(!ch[now][c]) ch[now][c]=++cnt;
		now=ch[now][c];
	
	idx[id]=now;
	return ;


void build() 
	queue<int>q;
	for(int i=0;i<26;i++)
		if(ch[root][i]) q.push(ch[root][i]),edge[root].push_back(ch[root][i]);
	while(!q.empty()) 
		int now=q.front();
		q.pop();
		for(int i=0;i<26;i++) 
			if(ch[now][i]) 
				fail[ch[now][i]]=ch[fail[now]][i];
				edge[ch[fail[now]][i]].push_back(ch[now][i]);
				q.push(ch[now][i]);
			
			else ch[now][i]=ch[fail[now]][i];
		
	
	return ;


void query(string s) 
	int now=root;
	for(int i=0;i<s.size();i++) 
		int c=s[i]-\'a\';
		now=ch[now][c];
		num[now]++;
	
	return ;


void dfs(int now) 
	for(int i=0;i<edge[now].size();i++)
		dfs(edge[now][i]),num[now]+=num[edge[now][i]];
	return ;


int main() 
	cin>>n;
	for(int i=1;i<=n;i++)
		cin>>s[i],insert(s[i],i);
	build();
	cin>>t;
	query(t);
	dfs(root);
	for(int i=1;i<=n;i++)
		cout<<num[idx[i]]<<endl;
	return 0;

AC 自动机

AC 自动机

1 算法简介

AC 自动机是一个以 Trie 为基础结合 KMP 的思想建立的。在 AC 自动机中,每一个状态代表着某个模式串的前缀,而整个 DFA 的结构其实是所有模式串的 Trie 树。

而 AC 自动机可以处理这样一个问题:多模式匹配。即给你若干个模式串和一个主串,要求我们对每一个字符串和主串进行匹配。

我们肯定不能做多次 KMP,所以我们有了 AC 自动机。

2 算法讲解

2.1 状态设计

struct node{
    int ch[26],end,fail;
};

其中,ch 数组是 Trie 树的指针,end 是判断这个状态为多少串的节点,fail 指针是后缀链接,指向具有相同后缀的状态。比如这个动图(其中的黄线是后缀链接):

其中 \\(2\\) 节点的指针画错了,应该是指向 \\(0\\)

2.2 插入

因为自身结构就是 Trie 的结构,所以 AC 自动机的插入和 Trie 树的插入是一模一样的。

代码:

	inline void insert(char *s){
        int now=0,len=strlen(s);
        for(int i=0;i<len;i++){
            int k=s[i]-\'a\';
            if(!p[now].ch[k]) p[now].ch[k]=++tot;
            now=p[now].ch[k];
        }
        p[now].end++;
    }

2.3 建立

在这里,我们需要建立 AC 自动机,Trie 树已经建好了,我们的目的是构建失配指针 fail 。暴力构建的话就是取其父节点,然后不断跳 fail ,直到调到一个状态,它有一条有相同字符的出边。但是这样时间复杂度不优,怎样优化?

我们先上代码:

inline void build(){
    queue<int> q;
    for(int i=0;i<26;i++) if(p[0].ch[i]) q.push(p[0].ch[i]);
    while(q.size()){
        int top=q.front();q.pop();
        for(int i=0;i<26;i++){
            if(p[top].ch[i]) p[p[top].ch[i]].fail=p[p[top].fail].ch[i],q.push(p[top].ch[i]);
            else p[top].ch[i]=p[p[top].fail].ch[i];
        }
    }
}

这里我们通过对跳 fail 的路径进行压缩,如果子节点存在,那就好说,我们把 fail 直接连过来就可以,但是如果不存在,我们就采用路径压缩,把其 fail 的子节点连过来,这样就完成了路径压缩。

放图:

2.4 查询

我们接下来分析查询函数 query ,这个函数将实现多模式匹配。我们直接放代码:

inline int query(char *t){
    int now=0,res=0,len=strlen(t);
    for(int i=0;i<len;i++){
        now=p[now].ch[t[i]-\'a\'];
        for(int j=now;j&&~p[j].end;j=p[j].fail) res+=p[j].end,p[j].end=-1;
    }
    return res;
}

什么意思?注意到我们对于 \\(t\\) 的每一个前缀,查询一下能匹配这个前缀的字符串,方法就是我们在 Trie 上走,然后对于每一个状态跳后缀链接,累加所有是结束节点的地方,注意因为可能重复调到同一个节点,所以我们要对所有的节点进行标记,这里 ~(-1)=0 ,这是一个很妙的打标记方式。

如果 Trie 树走到头了,会回到 \\(0\\) 号节点

2.5 时间复杂度分析

设所有模式串的总长为 \\(N\\),主串的长度为 \\(m\\) ,那么建立 Trie 树的复杂度为 \\(O(N)\\),查询时最坏情况是所有的后缀链接都被便利,为 \\(O(N)\\) ,再加上主串走 Trie 树,复杂度为 \\(O(m)\\)

所以总复杂度为 \\(O(N+m)\\)

2.6 总代码

#include<bits/stdc++.h>
#define dd double
#define ld long double
#define ll long long
#define uint unsigned int
#define ull unsigned long long
#define N 1000010
#define M number
using namespace std;

const int INF=0x3f3f3f3f;

template<typename T>  inline void read(T &x) {
    x=0; int f=1;
    char c=getchar();
    for(;!isdigit(c);c=getchar()) if(c == \'-\') f=-f;
    for(;isdigit(c);c=getchar()) x=x*10+c-\'0\';
    x*=f;
}

struct node{
    int ch[26],cnt,end,fail;
};

struct AC_automaton{
    node p[N];int tot;
    inline AC_automaton(){tot=0;}
    inline void insert(char *s){
        int now=0,len=strlen(s);
        for(int i=0;i<len;i++){
            int k=s[i]-\'a\';
            if(!p[now].ch[k]) p[now].ch[k]=++tot;
            now=p[now].ch[k];
        }
        p[now].end++;
    }
    inline void build(){
        queue<int> q;
        for(int i=0;i<26;i++) if(p[0].ch[i]) q.push(p[0].ch[i]);
        while(q.size()){
            int top=q.front();q.pop();
            for(int i=0;i<26;i++){
                if(p[top].ch[i]) p[p[top].ch[i]].fail=p[p[top].fail].ch[i],q.push(p[top].ch[i]);
                else p[top].ch[i]=p[p[top].fail].ch[i];
            }
        }
    }
    inline int query(char *t){
        int now=0,res=0,len=strlen(t);
        for(int i=0;i<len;i++){
            now=p[now].ch[t[i]-\'a\'];
            for(int j=now;j&&~p[j].end;j=p[j].fail) res+=p[j].end,p[j].end=-1;
        }
        return res;
    }
};
AC_automaton ac;

int n;
char s[N],t[N];

int main(){
    read(n);
    for(int i=1;i<=n;i++) scanf("%s",s),ac.insert(s);
    scanf("%s",t);
    ac.build();printf("%d",ac.query(t));
    return 0;
}

2.7 加强版

洛谷上有两次 AC 自动机加强版,分别是:

例题 1例题 2

对于例题 1,你只要把每个字符串出现的次数记下来排序就可以了,代码如下(码风略有点不同),注意 \\(end\\) 的含义稍微有点变化。

#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<cstring>
#include<sstream>
#include<queue>
#include<map>
#include<vector>
#include<set>
#include<deque>
#include<cstdlib>
#include<ctime>
#define dd double
#define ld long double
#define ll long long
#define ull unsigned long long
#define N 160
#define M 10001000
using namespace std;
 
const int INF=0x3f3f3f3f;
 
inline int read(){
	int x=0,f=1;
	char ch=getchar();
	while(ch<\'0\'||ch>\'9\'){if(ch==\'-\')f=-1;ch=getchar();}
	while(ch>=\'0\'&&ch<=\'9\'){x=x*10+ch-\'0\';ch=getchar();}
	return x*f;
}

struct rode{
    int id,sum;
    char s[N];
    inline bool operator < (const rode b){
        if(sum!=b.sum) return sum>b.sum;
        return id<b.id;
    }
};
rode a[N];

char t[M];

struct ACzdj{
    int tr[N*100][26],cnt;
    int end[N*100];int fail[N*100];
    
    inline void insert(char *s,int id){
        int p=0;
        for(int i=0;s[i];i++){
            int k=s[i]-\'a\';
            if(!tr[p][k]) tr[p][k]=++cnt;
            p=tr[p][k];
        }
        end[p]=id;
    }
    
    inline void build(){
        queue<int> q;
        memset(fail,0,sizeof(fail));
        for(int i=0;i<26;i++) if(tr[0][i]) q.push(tr[0][i]);
        while(q.size()){
            int top=q.front();q.pop();
            for(int i=0;i<26;i++){
                if(tr[top][i]){
                    fail[tr[top][i]]=tr[fail[top]][i];
                    q.push(tr[top][i]);
                }
                else tr[top][i]=tr[fail[top]][i];
            }
        }
    }
    
    inline void clear(){
        memset(tr,0,sizeof(tr));
        memset(end,0,sizeof(end));
        memset(fail,0,sizeof(fail));
        cnt=0;
    }
    
    inline int query(char *t){
        int p=0,res=0;
        for(int i=0;t[i];i++){
            p=tr[p][t[i]-\'a\'];
            for(int j=p;j;j=fail[j])
                a[end[j]].sum++;
        }
        return res;
    }
};
ACzdj ac;
 
int main(){
    while(1){
        memset(a,0,sizeof(a));
        ac.clear();
        int n=read();
        if(!n) break;
        for(int i=1;i<=n;i++){
            scanf("%s",a[i].s);
            ac.insert(a[i].s,i);
            a[i].id=i;
        }
        ac.build();
        scanf("%s",t);
        ac.query(t);
        sort(a+1,a+n+1);
        printf("%d\\n",a[1].sum);
        int now=1,minn=a[1].sum;
        while(a[now].sum==minn){
            printf("%s\\n",a[now].s);
            now++;
        }
    }
}

对于例题 2,这个题我们在查询的时候直接这样像例题 1 做,会超时,正确的做法是我们把 fail 树建出来,然后在树上 dp 合并就可以了。

代码:

#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<cstring>
#include<sstream>
#include<queue>
#include<map>
#include<vector>
#include<set>
#include<deque>
#include<cstdlib>
#include<ctime>
#define dd double
#define ld long double
#define ll long long
#define ull unsigned long long
#define N 1600000
#define M 10001000
using namespace std;
 
const int INF=0x3f3f3f3f;
 
inline int read(){
	int x=0,f=1;
	char ch=getchar();
	while(ch<\'0\'||ch>\'9\'){if(ch==\'-\')f=-1;ch=getchar();}
	while(ch>=\'0\'&&ch<=\'9\'){x=x*10+ch-\'0\';ch=getchar();}
	return x*f;
}

struct edge{
    int to,next;
    inline void intt(int to_,int ne_){
        to=to_;next=ne_;
    }
};
edge li[N*10];
int head[N*10],tail;

inline void add(int from,int to){
    li[++tail].intt(to,head[from]);
    head[from]=tail;
}

struct rode{
    int id,sum,belong,end;
};
rode a[N];

struct ACzdj{
    int tr[N*10][26],cnt,size[N*10];
    int end[N*10];int fail[N*10];
    
    inline int insert(char *s,int id){
        int p=0;
        for(int i=0;s[i];i++){
            int k=s[i]-\'a\';
            if(!tr[p][k]) tr[p][k]=++cnt;
            p=tr[p][k];
        }
        a[id].end=p;
        if(!end[p]) end[p]=id;
        else return end[p];
        return id;
    }
    
    inline void build(){
        queue<int> q;
        memset(fail,0,sizeof(fail));
        for(int i=0;i<26;i++) if(tr[0][i]) q.push(tr[0][i]);
        while(q.size()){
            int top=q.front();q.pop();
            for(int i=0;i<26;i++){
                if(tr[top][i]){
                    fail[tr[top][i]]=tr[fail[top]][i];
                    q.push(tr[top][i]);
                }
                else tr[top][i]=tr[fail[top]][i];
            }
        }
    }
    
    inline void clear(){
        memset(tr,0,sizeof(tr));
        memset(end,0,sizeof(end));
        memset(fail,0,sizeof(fail));
        cnt=0;
    }
    
    inline int query(char *t){
        int p=0,res=0;
        for(int i=0;t[i];i++){
            p=tr[p][t[i]-\'a\'];
//            for(int j=p;j;j=fail[j])
//                a[end[j]].sum++;
            size[p]++;
        }
        return res;
    }
};
ACzdj ac;

inline void dp(int k){
    for(int x=head[k];x;x=li[x].next){
        int to=li[x].to;
        dp(to);
        ac.size[k]+=ac.size[to];
    }
}

int n;
char s[M],t[M];

int main(){
    n=read();
    for(int i=1;i<=n;i++){
        a[i].id=i;
        scanf("%s",s);
        a[i].belong=ac.insert(s,i);
    }
    ac.build();
    scanf("%s",t);
    ac.query(t);
    for(int i=1;i<=ac.cnt;i++) add(ac.fail[i],i);
    dp(0);
    for(int i=1;i<=n;i++) printf("%d\\n",ac.size[a[i].end]);
//    for(int i=1;i<=n;i++) printf("%d\\n",a[a[i].belong].sum);
    return 0;
}

引用

以上是关于AC自动机的主要内容,如果未能解决你的问题,请参考以下文章

AC自动机基础

AC自动机详解

AC自动机

AC 自动机

AC自动机详解(附加可持久化AC自动机)

AC自动机