AC自动机
简介
Aho−Corasick automaton ,该算法在 1975 年产生于贝尔实验室,是著名的多模匹配算法。下面我们会举一个具体问题来讲解AC自动机。
前提知识
-
KMP
算法
对于问题:
找出长度 l 的字符串在长度为m 的字符串中出现次数?
KMP 给出了 O(l+m) 的做法 -
Trie
有 n 个单词,按照单词的前缀来把n 个单词构成一棵树,拥有相同前缀的单词在同一个分支上。
举个例子:
对于单词 say,her,he,shr,she
我们构建 Trie :
问题
给出
n
个单词
分析
不妨假设每一个单词的长度是 O(l) 级别的长度
- 原始想法
我们将每一次单词 pi 与 S 进行匹配,然后判断这个单词在不在该文章中。此时我们可以看出来这样做的复杂度是O(∑ni=1pi∗S)=O(n∗l∗S) 。显然这个复杂度太高了。 - 优化
每一次我们都需要将 S 与单词进行匹配,这个时候我们是可以使用KMP 算法来完成。那么我们就可以把整个复杂度降低到 O(n∗(l+S)) ,显然这个复杂度要优化很多。 - 再优化
我们是不是真的需要独立地去匹配每一个 pi 与 S ?如果我们在所有模式串中找到了一定的联系,当S 与其中一个匹配失败时,能够直接(或者说快速地)指向另外一个模式串,那我们将大大降低复杂度。再次利用 KMP 思想,我们把所有的模式串构建成一棵 Trie 树,然后 KMP 中失配时的 next 指针进行改成映射到树中某一个节点的 fail 指针。这样再进行匹配,那么总的复杂度就会变成 O(n∗l+S) 。
AC自动机
AC 自动机的算法过程:
-
n
个模式串构建
Trie 复杂度 O(n∗l) - 构建 fail 指针 O(n∗l)
- 进行匹配 O(n∗l+S)
构建 Trie :
我们如上图所示建立一棵 Trie ,代码如下:
void insert(char* str){
int len = strlen(str);
int u = rt;
FOR(i,0,len){
if(ch[u][str[i]-'a'] == -1){
newnode();
ch[u][str[i]-'a'] = sz;
}
u = ch[u][str[i]-'a'];
}
++ val[u];
}
val 数组用来记录该节点是不是结尾节点,即该分支是不是一个单词。很容易看出来,建立过程复杂度为 O(n∗l) 。
构建 fail 指针
我们用
bfs
来构建
fail
指针,首先了解
fail
指针干了一件什么事情?
fail
指针保证了能以
O(1)
的复杂度找到能与当前分支所构成的字符串匹配的最长公共后缀。
换句话说,如果当前分支为
root→a1→a2→a3→......→al
,其中
fail[al]=bp
,那么
fail
指针指向的分支为
root→b1→b2→b3→......→bp
,
fail
指针保证了
al−i==bp−i
对于任意
i<p
恒成立。
接下来我们用
bfs
来构造
fail
指针。
首先第一层的所有节点的
fail
指针都指向
root
,分别进入队列,然后
s
进入队列,查看子节点
以下代码做了做了优化:
void build(){
queue <int> q;
FOR(i,0,26){
if(ch[rt][i] == -1){
ch[rt][i] = rt;
}
else{
fail[ch[rt][i]] = rt;
q.push(ch[rt][i]);
}
}
while(!q.empty()){
int u = q.front(); q.pop();
FOR(i,0,26){
if(ch[u][i] == -1){
ch[u][i] = ch[fail[u]][i];
}
else{
fail[ch[u][i]] = ch[fail[u]][i];
q.push(ch[u][i]);
}
}
}
}
很容易看出复杂度为 O(n∗l)
匹配
设我们要记录的答案为
ans
。
以字符串
S=yasherhes
为例,我们扫描
S
串。设扫描到了
ch[u][S[i]] 存在,那么继续向下扫描, u 变成ch[u][i] , S[i] 点变成 S[i+1] 。-
ch[u][S[i]]
不存在,那么
u=fail[u]
,继续匹配
ch[u][S[i]]
,直到:
2.1 有一个节点匹配,那么 u 变成ch[u][i] , S[i] 点变成 S[i+1] 。
2.2 如果没有节点匹配,那么 u 变成root , S[i] 点变成 S[i+1] 。 按照以上规则匹配字符串 S ,
S[0]=y 字符失配,此时 u=root=0 。接下来匹配 S[1]=a ,继续失配。匹配 S[2]=s ,此时 u=1 。继续匹配 S[3]=h ,此时 u=7 。继续匹配 S[4]=e ,此时 u=9 ,发现这个节点是一个单词结尾,即 en[9]!=0 ,那么我们可以修改答案 ans=ans+1 ,修改 en[9]=0 。继续匹配 S[5]=r ,失配,那么 u=fail[9]=5 ,继续失配, u=fail[5]=0 ,失配, u=root 。匹配 S[6]=h , u=4 。继续匹配 S[7]=e , u=5 , ans=ans+1 。匹配 S[8] ,失配。结束,那么答案就是 2 。
附上代码:int query(char* str){ int len = strlen(str); int now = rt; int res = 0; FOR(i,0,len){ now = ch[now][str[i]-'a']; int temp = now; while(temp != rt){ res += val[temp]; val[temp] = 0; temp = fail[temp]; } } return res; }
这里的复杂度就是
O(n∗l+S) 。练习
-
ch[u][S[i]]
不存在,那么
u=fail[u]
,继续匹配
ch[u][S[i]]
,直到: