一、定义
AC自动机是一个复杂度为O(n) 的字符串匹配算法,能够实现快速多模板串匹配。
不多BB代码在文末
如待查找串有m个,分别为
模板串:
a
ab
abc
待匹配文本
模式串:
abcdcbab
则能快速找到
Matching "a" in pos 0, 6
Matching "ab" in pos 0, 6
Matching "abc" in pos 0
复杂度为三部分,
1、构建字典树,O(ml),m为模板串的个数,l为模板串的平均长度。
2、构建失配链接,O(ml)。
3、文本串匹配,复杂度为O(n),n为文本串长度。
前两个部分为离线预处理,第三部分为在线处理,实际总体复杂度为线性。
二、AC自动机构建
简单来说,AC自动机就是在模式串上跑字典树,指针A在模式串上跑,指针B同步在字典树上跑,A只会线性跑一次,复杂度为线性。
2.1字典树(Trie)
2.1.1定义
字典树又叫前缀树,是一个通过边来存储字符串的树。这个数据结构把前缀相同的字符串放在一起,节省空间,所以又叫前缀树。
如有6个模板串,分别为
she
he
say
shr
her
yasherhs
可见该串有很多部分是重复的前缀,按顺序插入字典树,则树是这个样子的
字典树不在节点存储字符,而是通过边来存储单个字符。如只会出现26个字母的话,每个节点都有26个孩子,代表出现的字符。如没有使用的孩子节点则直接为空。实心的节点则为该数据。可通过数组来实现这个过程,将字典树完全逻辑化。
typedef _Trie
{
char inf[MAXNODE][SIGMASIZE];
int next_node[MAXNODE][SIGMASIZE];
int value[MAXNODE];
int cnt;
} Trie;
其中,MAXNODE为字典树可能使用到的最大节点数量。一般以ml*来计算就足够使用了;SIGMASIZE为字符串匹配任务中可能出现的不同字符的种类数量,如26个字母任务则为26,中文匹配则为256。
next_node[i][j] 代表,i节点的第j个孩子,的节点编号。如上图中,根节点为0,其第 ‘y’ 个孩子的编号为10,则
next_node[0]['y'] = 10;
实际使用中一般如下,将字符映射为数组下标。
#define index(c) ((c) - 'a')
next_node[0][index('y')] = 10;
如为中文则将每个文字视为2字符,每个字符映射到1~256+1中 #define index( c ) (( c ) + 129)
如上图例中字典树
next_node[5][index('r')] = 9;
next_node[1][index('h')] = 2;
next_node[1][index('a')] = 6;
...
结构体中其余成员变量分别为
value[i]: i节点中是否具有实际数据。上图例中,实心节点的value值为1或该键值的数量,空心节点中value值为-1。(不涉及AC自动机的话,用0也行)
inf[i]: i节点中的数据实体字符串。
cnt: 字典树实际使用节点数量。
2.1.2构建
for循环跑一遍模板串集就好了,每个字符串插入一遍,插入时也是for循环跑他一轮,用一个指针从root开始,在字典树上移动,遇到已存在的节点就移动,不存在的节点就cnt++开辟新的节点就好(并非真的malloc,而是逻辑开辟)。在最终节点上记录信息。
#define at(i, j) ((i) * SIGMASIZE + (j))
#define index(c) (*trie->INDEX)(c)
/*****************************************************************
* Type name: Trie字典树
* Parameter: next_node[i][j]: i节点的第j儿子的编号
* inf_id[i]: i节点的信息对应外部字符串的编号
* value[i]: i节点是否有信息/相同信息的个数
* size: 字典树实际使用节点数
* MAXNODE: 字典树可使用节点数上限
* SIGMASIZE: 每个节点的儿子数量
* INDEX: 函数指针,传入字符映射关系
* Function: Trie_init: 字典树初始化,需要传入字典树实例、开
* 辟节点个数、容许出现的字符种类数量、
* 字符映射关系回调函数
* Trie_insert: 插入字符串,需要传入字典树实例、待插
* 入字符串、该字符串在模板集中的编号
* Trie_destroy: 析构函数,清理内存。
****************************************************************/
typedef struct _Trie
{
int *value;
int *inf_id;
int *next_node;
int size;
int MAXNODE;
int SIGMASIZE;
int (*INDEX)(char);
} Trie;
int Trie_init(Trie *trie, int MAXNODE, int SIGMASIZE, int (*INDEX)(char))
{
int i = 0;
trie->size = 1; //初始有1个root节点,编号为0
trie->MAXNODE = MAXNODE;
trie->SIGMASIZE = SIGMASIZE;
trie->INDEX = INDEX;
/* 使用一维数组来实现二维数组 */
trie->next_node = (int *)malloc(sizeof(int) * MAXNODE * SIGMASIZE);
trie->inf_id = (int *)malloc(sizeof(int) * MAXNODE);
trie->value = (int *)malloc(sizeof(int) * MAXNODE);
for (i = 0; i < SIGMASIZE; i++)
{
trie->next_node[at(0, i)] = -1;
}
trie->value[0] = -1;
return 0;
}
int Trie_insert(Trie *trie, char *str, int id)
{
int c = 0;
int i = 0, j = 0;
int now = 0;
int len = strlen(str);
int SIGMASIZE = trie->SIGMASIZE;
int *next_node = trie->next_node;
int *value = trie->value;
int *inf_id = trie->inf_id;
for (i = 0; i < len; i++)
{
c = index(str[i]);
if (next_node[at(now, c)] < 0)
{
next_node[at(now, c)] = trie->size;
for (j = 0; j < SIGMASIZE; j++)
next_node[at(trie->size, j)] = -1;
value[trie->size] = -1; //中间节点权值为-1
trie->size++;
}
now = next_node[at(now, c)];
}
/* 这部分决定是否去重 */
if (value[now] < 0)
value[now] = 1;
else
value[now]++;
inf_id[now] = id;
return 0;
}
int Trie_destroy(Trie *trie)
{
CHECK_NULL_POINTER(trie);
if (NULL != trie->next_node)
free(trie->next_node);
trie->next_node = NULL;
if (NULL != trie->value)
free(trie->value);
trie->value = NULL;
if (NULL != trie->inf_id)
free(trie->inf_id);
trie->inf_id = NULL;
return 0;
}
使用
int ind(char c);
inline int ind(char c){ return c - 'a'; }
int main()
{
Trie trie;
Trie_init(&trie, 100, 26, ind);
Trie_insert(&trie, "she", 0);
Trie_insert(&trie, "he", 1);
Trie_insert(&trie, "say", 2);
Trie_insert(&trie, "shr", 3);
Trie_insert(&trie, "her", 4);
Trie_destroy(&trie);
}
字典树构建完成。
字典树既可以做前缀树,也可以做后缀树,应在合适的时候选择合适的存储方法。
2.2将字典树转换成AC自动机
AC自动机实际上是对字典树的拓展,一般来说字典树的空儿子节点均为-1或者0,经过AC自动机的构建,空儿子节点可以去到,另一个模板串前缀与当前已达节点的后缀相同的节点。
例如三个模板串分别为abc、bc、c,那么当文本中匹配到了abc的时候,理应认为同样匹配到了bc和c。即当前已匹配部分具备另一个模板串的前缀。
考虑匹配一个模板串abc,模式串abd,暴力算法在匹配时会一个个的去对比:
模式串找到a,模板串找到a,匹配
模式串找到b,模板串找到b,匹配
模式串找到d,模板串找到c,不匹配
当遇到不匹配的时候,整个匹配过程又要从头开始,浪费时间。在字典树上的表现则是,now指针从root根节点开始,经过匹配,会走到树枝上,失配时,会直接回到根节点从头开始匹配。
而AC自动机则是构建失配链接,使得当找到d失配的情况下,能够直接快速跳转到以d为前缀的字符串的节点上。
例
模板串
she
he
say
shr
her
待匹配模式串
sshs
当读取到第一个s时,now指针从0节点走到1节点。
当读取到第二个s时,此时失配,没有任何模板串有ss前缀。则此时now指针应该仍处在1节点,其next_node[1][‘s’] = 1,因为ss已匹配后缀等于模板串的前缀,已经走过的路不应该重复走。
读取第三个字符h,now指针从1走到2。
读取第四个字符s,失配,但是匹配前缀s。now回到1。
同理,当走到3节点时,如遇到一个r,则应跳转至9节点。
只有当前后缀和任何前缀都不匹配时,才跳转回根节点。
完整构建AC自动机,则会得到一个比较复杂但是字典树的每个节点的子树都比较有用的 字典树。
上图模板集经过AC自动机构建,将变为下图的样子,实际则更为复杂。
红蓝绿线分别为第1、2、3层跳转关系。
2.3构建fail失配链接
首先增加一个失配链接fail数组,该数组指明当now处在i号节点时,如此时失配,跳转到fail[i]节点位置,然后再次进行匹配操作。fail数组是一个中间变量,构建AC自动机时使用,构建完成则不再使用。
失配链接的本质是,如果一个点i的fail指针指向j,那么root到j的字符串(前缀)是root到i的字符串的一个后缀。
如,根节点为第0层,由于第一层仅匹配了一个字符,任何字符都是自己的前缀,所以第一层所有的fail指针均指向根节点。当在第一层失配时,则回到根节点再次匹配。
next_node[now][i] = next_node[ fail[now] ][i];
比如now处在1节点,此时又遇到s,则next_node[1][‘s’] = next_node[root][‘s’];
匹配了的字符则继续往字典树下层走。
通过失配链接,则可以把下层的失配回流到上层某层重新进行匹配,而不是直接回到根。
在往下层走的同时,下层的失配链接等于当前节点失配时重新匹配的节点
fail[ next_node[now][i] ] = next_node[ fail[now] ][i];
fail[ next_node[1][‘h’] ] = next_node[ fail[1] ][‘h’]
fail[2] = next_node[0][‘h’]
fail[2] = 4
可见当在2号点失配时,则默认匹配了h,跳转到4号点继续运行。
具体方式通过广搜来构建,从第0层逐层向下递推。当走到某层的时候,该层的所有链接都已在上一层推出。推到底时,则AC自动机构建完成。
代码如下
首先在字典树数据结构中加入两个成员
typedef struct _Trie
{
/* 字典树成员变量 */
int *value;
int *inf_id;
int *next_node;
int size;
int MAXNODE;
int SIGMASIZE;
int (*INDEX)(char);
/* AC自动机成员变量 */
int *fail;
int *last;
} Trie;
int Aho_corasick_build(Trie *trie)
{
CHECK_NULL_POINTER(trie);
int i = 0;
Queue que;
Queue_init(que, trie->MAXNODE);
int SIGMASIZE = trie->SIGMASIZE;
int *next_node = trie->next_node;
int *value = trie->value;
int *inf_id = trie->inf_id;
trie->fail = (int *)malloc(sizeof(int) * trie->MAXNODE);
CHECK_NULL_POINTER(trie->fail);
trie->last = (int *)malloc(sizeof(int) * trie->MAXNODE);
CHECK_NULL_POINTER(trie->last);
int *fail = trie->fail;
int *last = trie->last;
fail[0] = 0;
for (i = 0; i < SIGMASIZE; i++)
{
if (next_node[at(0, i)] == -1)
next_node[at(0, i)] = 0; //回到根节点
else
{
fail[ next_node[at(0, i)] ] = 0;
Queue_push(que, next_node[at(0, i)]);//广搜将第一层入队
}
}
while (!Queue_empty(que))
{
int now = Queue_top(que);
Queue_pop(que);//printf("\nnow=%d\n", now);
for (i = 0; i < SIGMASIZE; i++)
{
if (next_node[at(now, i)] == -1)
next_node[at(now, i)] = next_node[at( fail[now] , i)];
else
{
fail[ next_node[at(now, i)] ] = next_node[at(fail[now], i)];
Queue_push(que, next_node[at(now, i)]);
//后缀链接。如果要去的是单词节点,last[下一个]就指向那个单词节点。
//否则指向上一层的那个,直至根节点。
if (value[ fail[ next_node[at(now, i)] ] ] > 0)
last[ next_node[at(now, i)] ] = fail[ next_node[at(now, i)] ];
else
last[ next_node[at(now, i)] ] = last[ fail[ next_node[at(now, i)] ] ];
}
// if (next_node[at(now, i)] > 0)
// printf("next[%d][%c]=%d last[%d]=%d\n", now, i + 'a', next_node[at(now, i)], now, last[now]);
}
}
Queue_destroy(que);
return 0;
}
代码中的last链接为后缀链接。如匹配到abc时,应认为bc、c均匹配成功。last[abc]=bc, last[bc]=c按顺序向上层映射,当找到某个词时,递归地向上查找即可拿到所有的和改词后缀相同的词。
构建结果,SIGMASIZE中未输出的都是直接跳转到根节点,为0
now=0
next[0][h]=4 last[0]=0
next[0][s]=1 last[0]=0
now=4
next[4][e]=5 last[4]=0
next[4][h]=4 last[4]=0
next[4][s]=1 last[4]=0
now=1
next[1][a]=6 last[1]=0
next[1][h]=2 last[1]=0
next[1][s]=1 last[1]=0
now=5
next[5][h]=4 last[5]=0
next[5][r]=9 last[5]=0
next[5][s]=1 last[5]=0
now=6
next[6][h]=4 last[6]=0
next[6][s]=1 last[6]=0
next[6][y]=7 last[6]=0
now=2
next[2][e]=3 last[2]=0
next[2][h]=4 last[2]=0
next[2][r]=8 last[2]=0
next[2][s]=1 last[2]=0
now=9
next[9][h]=4 last[9]=0
next[9][s]=1 last[9]=0
now=7
next[7][h]=4 last[7]=0
next[7][s]=1 last[7]=0
now=3
next[3][h]=4 last[3]=5
next[3][r]=9 last[3]=5
next[3][s]=1 last[3]=5
now=8
next[8][h]=4 last[8]=0
next[8][s]=1 last[8]=0
可对比字典树的例图,可见构建成功。其中last[3]=5则代表,3节点的she,其后缀具有5节点的he,即3词的后缀完整包含5词。
三、AC自动机运行
运行起来就轻松了。进入find函数去跑,因为整个字典树已经构建成闭环,遇到任何字符都可以直接在树上跑。
然后每个字符检查一下该节点是否有键值(value是否为1),如果有键值就进入递归打印阶段
递归打印阶段中,通过后缀链接递归地向上层找,直至根节点。(找到abc应认为找到了bc和c)
最终结果通过队列或者什么数据结构带出即可。
int recursion_print(Trie *trie, int now, int i, Queue *res)
{
if (now > 0)
if (trie->value[now] > 0)
{
int tmp = trie->value[now];
Queue_push(*res, i);
Queue_push(*res, trie->inf_id[now]);
// trie->value[now] = 0; //1、
tmp += recursion_print(trie, trie->last[now], i, res);
// trie->last[now] = 0; //2、这两行决定查找到的模板是否重复计算
return tmp;
}
return 0;
}
int Aho_corasick_find(Trie *trie, char *txt, Queue *res)
{
CHECK_NULL_POINTER(trie);
CHECK_NULL_POINTER(txt);
int len = strlen(txt);
int sum = 0, i = 0;
int now = 0;
int SIGMASIZE = trie->SIGMASIZE;
int *next_node = trie->next_node;
int *value = trie->value;
int *inf_id = trie->inf_id;
int *last = trie->last;
for (i = 0; i < len; i++)
{
int ch = index(txt[i]);
now = next_node[at(now, ch)];
if (value[now] > 0)
sum += recursion_print(trie, now, i, res);
else
if (last[now] > 0)
sum += recursion_print(trie, last[now], i, res);
}
return sum;
}
如对上文提及的模板串集去跑"shesay"这个模式串,则可得到
shesay
In pos 2 get string[0]:she
In pos 2 get string[1]:he
In pos 5 get string[2]:say
四、完整代码与运行方法
完整c版代码如下,上节代码中使用的队列在头文件中有实现。
https://download.csdn.net/download/aaddggddaa/12910824
头文件
/*****************************************************************
* File name: ac_automaton.h
****************************************************************/
#ifndef __AC_AUTOMATON_H__
#define __AC_AUTOMATON_H__
/* 在int返回值时,进行是否为空判断 */
#define CHECK_NULL_POINTER(p)\
do\
{\
if (NULL == (p))\
{\
printf("Pointer is equal to NULL\n");\
return -1;\
}\
} while (0)
/*****************************************************************
* Type name: Trie字典树
* Parameter: next_node[i][j]: i节点的第j儿子的编号
* inf_id[i]: i节点的信息对应外部字符串的编号
* value[i]: i节点是否有信息/相同信息的个数
* size: 字典树实际使用节点数
* MAXNODE: 字典树可使用节点数上限
* SIGMASIZE: 每个节点的儿子数量
* INDEX: 函数指针,传入字符映射关系
****************************************************************/
typedef struct _Trie
{
/* 字典树成员变量 */
int *value;
int *inf_id;
int *next_node;
int size;
int MAXNODE;
int SIGMASIZE;
int (*INDEX)(char);
/* AC自动机成员变量 */
int *fail;
int *last;
} Trie;
int Trie_init(Trie *trie, int MAXNODE, int SIGMASIZE, int (*INDEX)(char));
int Trie_destroy(Trie *trie);
int Trie_insert(Trie *trie, char *str, int id);
/*****************************************************************
* Data type: 简单int循环队列
* Function: Queue_init: 初始化队列,申请空间
* Queue_push: 入队一个元素
* Queue_empty: 检查队列是否为空
* Queue_pop: 出队一个元素
* Queue_top: 查看队头元素
* Queue_destroy:释放空间
****************************************************************/
typedef struct _Queue
{
int *queue_data;
int phead, ptail;
int size, MAXSIZE;
} Queue;
#define Queue_init(que, si)\
do\
{\
(que).queue_data = NULL;\
(que).queue_data = (int *)malloc(sizeof(int) * (si));\
CHECK_NULL_POINTER((que).queue_data);\
(que).phead = (que).ptail = 0;\
(que).MAXSIZE = (si);\
(que).size = 0;\
} while (0)
#define Queue_push(que, num)\
do\
{\
if ((que).size >= (que).MAXSIZE)\
break;\
(que).queue_data[(que).phead] = (num);\
(que).phead = (que).phead + 1 >= (que).MAXSIZE ? 0 : (que).phead + 1;\
(que).size++;\
} while (0)
#define Queue_empty(que) (que).size <= 0 ? 1 : 0
#define Queue_pop(que)\
do\
{\
if ((que).size <= 0)\
break;\
(que).size--;\
(que).queue_data[(que).ptail] = 0;\
(que).ptail = (que).ptail + 1 >= (que).MAXSIZE ? 0 : (que).ptail + 1;\
} while (0)
#define Queue_top(que) (que).queue_data[(que).ptail]
#define Queue_destroy(que)\
do\
{\
if (NULL != (que).queue_data)\
free((que).queue_data);\
(que).queue_data = NULL;\
(que).phead = (que).ptail = 0;\
} while (0)
/*****************************************************************
* Type name: AC自动机
* Parameter: fail[i]: i节点的失配链接
* last[i]: i节点的后缀链接
****************************************************************/
int Aho_corasick_build(Trie *trie);
int Aho_corasick_find(Trie *trie, char *txt, Queue *res);
int Aho_corasick_destroy(Trie *trie);
#endif
C文件
/*****************************************************************
* Aho-Corasick automaton
* File name: ac_automaton.c
* Note: AC自动机,多模板串单模式串快速字符串匹配算法
* 已将模板文件解耦
* 查询结果通过头文件自带队列带出,队列中依次为匹
* 配到的位置和匹配字符串在模板集中的序号
* 头文件中自带队列实现
* Use: 1、Trie t + Trie_init 声明字典树并初始化
* 2、Trie_insert 循环插入模板串
* 3、Aco_corasick_build 构建AC自动机
* 4、Queue + Queue_init 声明并初始化结果队列
* 5、Aco_corasick_find AC自动机搜索模式串
* 6、Aco_corasick_destroy AC自动机析构
* 7、Trie_destroy 字典树析构
* 8、Queue_destroy 结果队列析构
* Author: PlatixYe
* Date: 2020.9.29
****************************************************************/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "ac_automaton.h"
/*****************************************************************
* Data type: Trie字典树部分
* Function: Trie_init: 字典树初始化,需要传入字典树实例、开
* 辟节点个数、容许出现的字符种类数量、
* 字符映射关系回调函数
* Trie_insert: 插入字符串,需要传入字典树实例、待插
* 入字符串、该字符串在模板集中的编号
* Trie_destroy:析构函数,清理内存。
****************************************************************/
#define at(i, j) ((i) * SIGMASIZE + (j))
#define index(c) (*trie->INDEX)(c)
int Trie_init(Trie *trie, int MAXNODE, int SIGMASIZE, int (*INDEX)(char))
{
CHECK_NULL_POINTER(trie);
int i = 0;
trie->size = 1; //初始有1个root节点,编号为0
trie->MAXNODE = MAXNODE;
trie->SIGMASIZE = SIGMASIZE;
trie->INDEX = INDEX;
/* 使用一维数组来实现二维数组 */
trie->next_node = (int *)malloc(sizeof(int) * MAXNODE * SIGMASIZE);
trie->inf_id = (int *)malloc(sizeof(int) * MAXNODE);
trie->value = (int *)malloc(sizeof(int) * MAXNODE);
for (i = 0; i < SIGMASIZE; i++)
{
trie->next_node[at(0, i)] = -1;
}
trie->value[0] = -1;
return 0;
}
int Trie_insert(Trie *trie, char *str, int id)
{
CHECK_NULL_POINTER(trie);
CHECK_NULL_POINTER(str);
int c = 0;
int i = 0, j = 0;
int now = 0;
int len = strlen(str);
int SIGMASIZE = trie->SIGMASIZE;
int *next_node = trie->next_node;
int *value = trie->value;
int *inf_id = trie->inf_id;
for (i = 0; i < len; i++)
{
c = index(str[i]);
if (next_node[at(now, c)] < 0)
{
next_node[at(now, c)] = trie->size;
for (j = 0; j < SIGMASIZE; j++)
next_node[at(trie->size, j)] = -1;
value[trie->size] = -1; //中间节点权值为-1
trie->size++;
}
now = next_node[at(now, c)];
}
/* 这部分决定是否去重 */
if (value[now] < 0)
value[now] = 1;
else
value[now]++;
inf_id[now] = id;
return 0;
}
int Trie_destroy(Trie *trie)
{
CHECK_NULL_POINTER(trie);
if (NULL != trie->next_node)
free(trie->next_node);
trie->next_node = NULL;
if (NULL != trie->value)
free(trie->value);
trie->value = NULL;
if (NULL != trie->inf_id)
free(trie->inf_id);
trie->inf_id = NULL;
return 0;
}
/*****************************************************************
* Data type: AC自动机部分
* Function: Aho_corasick_build: 将字典树构建为AC自动机,需要传
* 入字典树实例
* Aho_corasick_find: 查询模式串中有多少匹配上的模板
* 串需要传入字典树实例、待搜索字
* 符串、接收结果用的自带队列
* Aho_corasick_destroy:析构函数,清理内存。
****************************************************************/
int Aho_corasick_build(Trie *trie)
{
CHECK_NULL_POINTER(trie);
int i = 0;
Queue que;
Queue_init(que, trie->MAXNODE);
int SIGMASIZE = trie->SIGMASIZE;
int *next_node = trie->next_node;
int *value = trie->value;
int *inf_id = trie->inf_id;
trie->fail = (int *)malloc(sizeof(int) * trie->MAXNODE);
CHECK_NULL_POINTER(trie->fail);
trie->last = (int *)malloc(sizeof(int) * trie->MAXNODE);
CHECK_NULL_POINTER(trie->last);
int *fail = trie->fail;
int *last = trie->last;
fail[0] = 0;
for (i = 0; i < SIGMASIZE; i++)
{
if (next_node[at(0, i)] == -1)
next_node[at(0, i)] = 0; //回到根节点
else
{
fail[ next_node[at(0, i)] ] = 0;
Queue_push(que, next_node[at(0, i)]);//广搜将第一层入队
}
}
while (!Queue_empty(que))
{
int now = Queue_top(que);
Queue_pop(que);//printf("\nnow=%d\n", now);
for (i = 0; i < SIGMASIZE; i++)
{
if (next_node[at(now, i)] == -1)
next_node[at(now, i)] = next_node[at( fail[now] , i)];
else
{
fail[ next_node[at(now, i)] ] = next_node[at(fail[now], i)];
Queue_push(que, next_node[at(now, i)]);
//后缀链接。如果要去的是单词节点,last[下一个]就指向那个单词节点。
//否则指向上一层的那个,直至根节点。
if (value[ fail[ next_node[at(now, i)] ] ] > 0)
last[ next_node[at(now, i)] ] = fail[ next_node[at(now, i)] ];
else
last[ next_node[at(now, i)] ] = last[ fail[ next_node[at(now, i)] ] ];
}
// if (next_node[at(now, i)] > 0)
// printf("next[%d][%c]=%d last[%d]=%d\n", now, i + 'a', next_node[at(now, i)], now, last[now]);
}
}
Queue_destroy(que);
return 0;
}
int recursion_print(Trie *trie, int now, int i, Queue *res)
{
if (now > 0)
if (trie->value[now] > 0)
{
int tmp = trie->value[now];
Queue_push(*res, i);
Queue_push(*res, trie->inf_id[now]);
// trie->value[now] = 0; //1、
tmp += recursion_print(trie, trie->last[now], i, res);
// trie->last[now] = 0; //2、这两行决定查找到的模板是否重复计算
return tmp;
}
return 0;
}
int Aho_corasick_find(Trie *trie, char *txt, Queue *res)
{
CHECK_NULL_POINTER(trie);
CHECK_NULL_POINTER(txt);
int len = strlen(txt);
int sum = 0, i = 0;
int now = 0;
int SIGMASIZE = trie->SIGMASIZE;
int *next_node = trie->next_node;
int *value = trie->value;
int *inf_id = trie->inf_id;
int *last = trie->last;
for (i = 0; i < len; i++)
{
int ch = index(txt[i]);
now = next_node[at(now, ch)];
if (value[now] > 0)
sum += recursion_print(trie, now, i, res);
else
if (last[now] > 0)
sum += recursion_print(trie, last[now], i, res);
}
return sum;
}
int Aho_corasick_destroy(Trie *trie)
{
CHECK_NULL_POINTER(trie);
if (NULL != trie->fail)
free(trie->fail);
trie->fail = NULL;
if (NULL != trie->last)
free(trie->last);
trie->last = NULL;
return 0;
}
使用方法
/*****************************************************************
* File name: using.c 使用方法
****************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include "ac_automaton.h"
#define MAXWORD 100
#define SIGMASIZE 26
int ind(char c);
inline int ind(char c){ return c - 'a'; }
char str[5][5] ={ "she", "he", "say", "shr", "her" };
int main()
{
Trie trie;
int i = 0;
Trie_init(&trie, MAXWORD, SIGMASIZE, ind);
for (i = 0; i < 5; i++)
Trie_insert(&trie, str[i], i);
Aho_corasick_build(&trie);
Queue res;
Queue_init(res, MAXWORD);
Aho_corasick_find(&trie, "shesay", &res);
while (!Queue_empty(res))
{
int pos, id;
pos = Queue_top(res);
Queue_pop(res);
id = Queue_top(res);
Queue_pop(res);
printf("In pos %d get string[%d]:%s\n", pos, id, str[id]);
}
Aho_corasick_destroy(&trie);
Trie_destroy(&trie);
Queue_destroy(res);
return 0;
}
ACM单代码版
//AC自动机,CPP单文件版
#include<iostream>//可以查出被访问了多少个模板串。
#include<cstdio>
#include<cstring>
#include<queue>
using namespace std;
const int Max_Node=1000002,Sigma_Size=26,muban_Size=52,muban_Num=10010;
int n;
char txt[Max_Node];
int idx(char c){return c-'a';}
class trie
{
public:
int value[muban_Size*muban_Num];
int fail[muban_Size*muban_Num];//失配指针。
int last[muban_Size*muban_Num];//后缀链接。
//char information[muban_Size*muban_Num][muban_Size];
int next_node[muban_Size*muban_Num][Sigma_Size];
int trie_size;
trie()
{
// clear();
trie_size=1;// 初始有1个根节点。根节点为0号点。
for(int i=0;i<Sigma_Size;i++)
next_node[0][i]=-1;
value[0]=-1;
}
void clear()
{
memset(next_node,0,(trie_size+10)*4);
memset(fail,0,(trie_size+10)*4);
memset(last,0,(trie_size+10)*4);
memset(value,0,(trie_size+10)*4);
trie_size=1;// 初始有1个根节点。根节点为0号点。
for(int i=0;i<Sigma_Size;i++)
next_node[0][i]=-1;
value[0]=-1;
}
int insert(char s[])//字典树创建。
{
int now=0;
int len=strlen(s);
for(int i=0;i<len;i++)
{
if(next_node[now][idx(s[i])]<=0)
{
next_node[now][idx(s[i])]=trie_size;
for(int j=0;j<Sigma_Size;j++)
next_node[trie_size][j]=-1;
value[trie_size]=-1;//中间节点权值为-1.
trie_size++;
}
now=next_node[now][idx(s[i])];
}
if(value[now]<0)
value[now]=1;
else
value[now]++;
// strcpy(information[now],s);
return 0;
}
int Aho_Corasick_build()//将字典树创建成AC自动机。
{
queue<int> Q;
fail[0]=0;
for(int i=0;i<Sigma_Size;i++)
{
if(next_node[0][i]==-1)
next_node[0][i]=0;
else
{
fail[next_node[0][i]]=0;
Q.push(next_node[0][i]);
}
}
while(!Q.empty())
{
int now=Q.front();
Q.pop();
for(int i=0;i<Sigma_Size;i++)
{
if(next_node[now][i]==-1)
next_node[now][i]=next_node[fail[now]][i];
else
{
fail[next_node[now][i]]=next_node[fail[now]][i];
Q.push(next_node[now][i]);
//后缀链接。如果要去的节点是单词节点,last[改点]就指向那个单词节点。否则指向再上一层的那个,直至指向根节点。
if(value[fail[next_node[now][i]]]>0)
last[next_node[now][i]]=fail[next_node[now][i]];
else
last[next_node[now][i]]=last[fail[next_node[now][i]]];
}
}
}
return 0;
}
int find(char txt[])//AC自动机动起来!
{
int len=strlen(txt);
int sum=0;
int now=0;
// while(scanf("%c",&tamp_c)!=EOF&&(tamp_c!=10&&tamp_c!=13))
for(int i=0;i<len;i++)
{
int ch=idx(txt[i]);
now=next_node[now][ch];
if(value[now]>0)
sum+=print(now);
else
if(last[now]>0)
sum+=print(last[now]);
}
return sum;
}
int print(int now)//递归按照后缀指针打印。
{
if(now>0)
{
// printf("Get : %s\n",information[now]);
if(value[now]>0)
{
int tamp=value[now];
//value[now]=0; //1、
tamp=print(last[now])+tamp;
//last[now]=0; //2、这两个东西决定了数据是否重复计算
return tamp;
}
else return 0;
}
else return 0;
}
}ac;
int main()
{
int id;
char s[muban_Size];
scanf("%d",&id);
while(id--)
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%s",s);//puts(s);
ac.insert(s);
}
// scanf("%s",txt);
ac.Aho_Corasick_build();
scanf("%s",txt);
printf("%d\n",ac.find(txt));
// ac.search(s);
ac.clear();
}
return 0;
}