AC 自动机就是在 trie 上做 KMP ,先构造所有字符串的一个 trie ,再添加失败边(failure links),失败边跟 KMP 里的“失败函数”是一样的道理。
trie树实际上是一个DFA(Deterministic finite automaton),通常用转移矩阵表示。行表示状态,列表示输入字符,(行, 列)位置表示转移状态。这种方式的查询效率很高,但由于稀疏的现象严重,空间利用效率很
低。也可以采用压缩的存储方式即链表来表示状态转移,但由于要线性查询,会造成效率低下。
这是一个统计词频的trie树实例(来自wiki)
#include <stdio.h> #include <stdlib.h> #include <string.h> #define TREE_WIDTH 256 #define WORDLENMAX 128 struct trie_node_st { int count; struct trie_node_st *next[TREE_WIDTH]; }; static struct trie_node_st root={0, {NULL}}; static char *spaces=" \t\n/.\"\'()"; static int insert(const char *word) { int i; struct trie_node_st *curr, *newnode; if (word[0]=='\0') { return 0; } curr = &root; for (i=0; ; ++i) { if (word[i] == '\0') { break; } if (curr->next[ word[i] ] == NULL) { newnode=(struct trie_node_st*)malloc(sizeof(struct trie_node_st)); memset(newnode, 0, sizeof(struct trie_node_st)); curr->next[ word[i] ] = newnode; } curr = curr->next[ word[i] ]; } curr->count ++; return 0; } static void printword(const char *str, int n) { printf("%s\t%d\n", str, n); } static int do_travel(struct trie_node_st *rootp) { static char worddump[WORDLENMAX+1]; static int pos=0; int i; if (rootp == NULL) { return 0; } if (rootp->count) { worddump[pos]='\0'; printword(worddump, rootp->count); } for (i=0;i<TREE_WIDTH;++i) { worddump[pos++]=i; do_travel(rootp->next[i]); pos--; } return 0; } int main(void) { char *linebuf=NULL, *line, *word; size_t bufsize=0; int ret; while (1) { ret=getline(&linebuf, &bufsize, stdin); if (ret==-1) { break; } line=linebuf; while (1) { word = strsep(&line, spaces); if (word==NULL) { break; } if (word[0]=='\0') { continue; } insert(word); } } /* free(linebuf); */ do_travel(&root); exit(0); }
但这种算法效率比较低,没有充分利用已经计算出来的信息。网上流传的是另外一种算法:比如要找 she 的失败边,可以假定其父节点,sh 的失败边已经求出,沿着父节
点的失败边走,如果某个节点有 e 的子节点,则把失败边指向该子节点。比如 she 的父节点 sh ,先走到 h ,假如 h 没有 e 的子节点的话,就继续检查 h 的失败边,即
root 。如果最后到 root 都没有,就把失败边置为 root 。
匹配
在 AC 自动机上匹配:从根开始,如果当前字符匹配,则移动指针。然后需要沿着失败边检查:看有没有哪个是结尾节点,是则匹配成功(网上有些代码是只有当前整个串匹配
成功才找,但实际上只要当前字符是一样的就要找)。比如如果用上面的 AC 自动机匹配 she ,匹配到 she 的时候,she 这个单词匹配成功,还要沿失败边检查,发现 he
和 e 也匹配成功。如果当前字符不匹配,则沿着失败边继续找,如果都没有匹配,则回到 root 。
1 const int kind = 26;
2 struct node{
3 node *fail; //失败指针
4 node *next[kind]; //Tire每个节点的个子节点(最多个字母)
5 int count; //是否为该单词的最后一个节点
6 node(){ //构造函数初始化
7 fail=NULL;
8 count=0;
9 memset(next,NULL,sizeof(next));
10 }
11 }*q[500001]; //队列,方便用于bfs构造失败指针
12 char keyword[51]; //输入的单词
13 char str[1000001]; //模式串
14 int head,tail; //队列的头尾指针
1 void insert(char *str,node *root){
2 node *p=root;
3 int i=0,index;
4 while(str[i]){
5 index=str[i]-'a';
6 if(p->next[index]==NULL) p->next[index]=new node();
7 p=p->next[index];
8 i++;
9 }
10 p->count++; //在单词的最后一个节点count+1,代表一个单词
11 }
1 int query(node *root){
2 int i=0,cnt=0,index,len=strlen(str);
3 node *p=root;
4 while(str[i]){
5 index=str[i]-'a';
6 while(p->next[index]==NULL && p!=root) p=p->fail;
7 p=p->next[index];
8 p=(p==NULL)?root:p;
9 node *temp=p;
10 while(temp!=root && temp->count!=-1){
11 cnt+=temp->count;
12 temp->count=-1;
13 temp=temp->fail;
14 }
15 i++;
16 }
17 return cnt;
18 }