概述
Aho-Corasick automaton(AC自动机),该算法在1975年产生于贝尔实验室,是著名的多模匹配算法之一,该算法巧妙地将多模式串P建成一个确定有穷自动机(DFA),以待匹配字符串T作为该DFA的输入,使状态机进行状态转移(图2的0号节点为初态),当到达某些特定的状态时(终态:有模式串被匹配成功),完成模式匹配,能在 时间内完成多模式匹配(其中n为待匹配字符串的长度)。一个简单的例子,给出n个模式串P1,P2,P3...Pn,再给出一连续文本T1...m,问你这n个单词中,有多少个在该文本中出现过。AC自动机的创建可分为三个步骤:Trie树的构建;构造失配指针;模式匹配过程。
1.Trie树(字典树)的构建
Trie树在初始状态时,仅含有一个根节点。将所有模式串插入到Trie树中,若当前插入的字符已存在某条路径上,则访问这条路径所指向的节点,否则就新建一个指向当前带插入字符的节点,若一个模式串已经被完全插入,则标记其最后一个节点。
Trie树的三个基本特征:
- Trie树的每一条边上都有且仅有一个字符;
2、从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串;
3、与同一个节点相连的边上的字符均不相同。
实例:以下给出一个模式集合P={she, he, say, her, shr},对应的Trie树如图1所示。圆圈表示节点,粗圈表示一个字符串的结束。顶端的节点为根节点,作为Trie树入口。
图1 Trie树
2.构造失配指针(fail)
构造作用:使当前字符失配时跳转到的模式串与当前模式串具有最长公共前后缀。
构造原理:通过BFS(Breadth-First Search),即广度优先算法,按照层次遍历,根据父节点的fail指针来构造孩子节点的fail指针,以确保找到最长公共前后缀。
失配指针,顾名思义,即匹配到某一节点失败时,确定转移到的节点位置。需要注意的是,每个节点的fail指针指向的是以当前节点表示的字符为最后一个字符的最长当前字符串的后缀字符串的最后一个节点。那如何找到这个最长的当前字符串的后缀?以图2为例,(图中的数字是构造各节点fail指针的遍历顺序,虚线代表fail指针指向)如果当前已到达8号节点,要找到与she具有最长公共前后缀的模式串,那么就沿着当前节点的父节点(5号节点)失配指针走,如果fail指针所指向的1号节点的转移到它各孩子节点的路径上没有字母e(这里含有e,指向的是3号节点),则继续沿着1号节点的fail指针向上走,直到找到到达某节点的路径上含有字母e,则将8号节点指向这条路径所指向的节点,如果一直到了根节点(0号节点)都没找到,则将该节点的fail指针指向根节点。
图2 构造fail指针
3.模式匹配过程
匹配过程分两种情况:
(1)当前字符匹配,表示从当前节点沿着树边有一条路径可以到达目标字符,此时只需沿该路径走向下一个节点继续匹配即可,目标字符串指针移向下个字符继续匹配;
(2)当前字符不匹配,则去当前节点失败指针所指向的字符继续匹配,匹配过程随着指针指向0号节点(根节点)结束。重复这两个过程中的任意一个,直到模式串走到结尾为止。
实例:给出一个待匹配字符串ashers,其完整的状态转移图如图3所示。匹配过程中遇到粗圈的节点时,则表示成功的匹配到某模式串。匹配到的模式串有she、her。
图3 ashers状态转移图
4.实现代码
以下以杭电2222为例: http://acm.hdu.edu.cn/showproblem.php?pid=2222
#include <cstdio>
#include <queue>
#include <cstring>
#include <iostream>
#include <algorithm>
#define MAX_L 500005
#define MAX_P 1000006
#define LETTRE_CNT 26
using namespace std;
struct Aho {
struct Trie {
int next[LETTRE_CNT];
int fail;
int state; //1表示该处已有一个单词结束
}trie[MAX_L];
queue<int> que;
int size;
//初始化根结点
void init() {
while(!que.empty()) {
que.pop();
}
memset(trie[0].next,0,sizeof(trie[0].next));
trie[0].fail = trie[0].state = 0;
size = 1;
}
//建立树
void insert(char *s) {
int now = 0;
int sLen = strlen(s);
for(int i = 0; i < sLen; i++) {
int c = s[i] - 'a';
if(!trie[now].next[c]) {
memset(trie[size].next,0,sizeof(trie[size].next));
trie[size].fail = trie[size].state = 0;
trie[now].next[c] = size++;
}
now = trie[now].next[c];
}
trie[now].state++;
}
//建立失配指针
void bulid(char *s) {
trie[0].fail = -1;
que.push(0);
while(!que.empty()){
int now = que.front();
que.pop();
for(int i = 0; i < LETTRE_CNT; i++) {
if(trie[now].next[i]) {
if(now == 0) trie[trie[now].next[i]].fail = 0;
else {
int v = trie[now].fail;
while(v != -1) {
if(trie[v].next[i]) {
trie[trie[now].next[i]].fail = trie[v].next[i];
break;//确保找到的是最长公共串
}
v = trie[v].fail;//没有就一直往上找祖先
}
if(v == -1) {
trie[trie[now].next[i]].fail = 0;
}
}
que.push(trie[now].next[i]);
}
}
}
}
int Get(int now) {
int res = 0;
while(now) {
res += trie[now].state;
trie[now].state = 0;
now = trie[now].fail;
}
return res;
}
int match(char *s) {
int now = 0;
int res = 0;//保存匹配到的单词的个数
int sLen = strlen(s);
for(int i = 0; i < sLen; i++) {
int c = s[i] - 'a';
if(trie[now].next[c]) {
now = trie[now].next[c];
}
else {
int p = trie[now].fail;
while(p != -1 && !trie[p].next[c]) {
p = trie[p].fail;
}
if(p == -1) {
now = 0;
}else {
now = trie[p].next[c];
}
}
if(trie[now].state) {
res += Get(now);
}
}
return res;
}
}aho;
char S[MAX_P];
int main() {
int n;
scanf("%d",&n);
while(n--) {
aho.init();
int cnt;
scanf("%d",&cnt);
for(int i = 0; i < cnt; i++) {
scanf("%s",S);
aho.insert(S);
}
aho.bulid(S);
scanf("%s",S);
printf("%d\n",aho.match(S));
}
return 0;
}
如有不当,欢迎指出!!!