AC 自动机
原文:http://www.cppblog.com/mythit/archive/2009/04/21/80633.html
感谢极限定理的详细解答,对于AC自动机终于算是弄明白了。
下面的是自己的一些理解。(依旧是以HDU2222为例,插图也是极限定理的,思路是极限定理的,代码可是自己敲的)
在AC自动机主要有下面几步:
- 构建字母树,即trie 树。
- 构造失败指针
- 查找
详细步骤:
- 构建trie树
- 构建一个根节点
1 node *root = new node;
- 插入单词
1 void insert(char *str, node *root) 2 { 3 node *p = root; 4 int i=0, index; 5 while (str[i]) 6 { 7 index = str[i]-'a'; 8 if (p->next[index] == NULL) //若子节点没有则新建一个节点 9 p->next[index] = new node; 10 p = p->next[index]; //指向下一个点 11 i++; 12 } 13 p->flg++; //在单词的最后一个节点加1,以便在后面的查询中使用 14 }
- 构建失败指针
构建失败指针的目的就是在查找的时候可以按照失败指针跳转。如kmp 的next 的指针一样。
1 void build_AC(node *root) 2 { 3 int head=0, tail=0, i; 4 root->fail = NULL; //根节点的失败指针位NULL 5 Q[head++] = root; //根节点入栈 6 while (head !=tail) 7 { 8 node *tmp = Q[tail++]; 9 node *p = NULL; 10 for (i=0; i<kind; i++) 11 { 12 if (tmp->next[i] != NULL) //若该单词存在则进入 13 { 14 if (tmp == root) //根节点的子节点,失败指针都指向root 15 tmp->next[i]->fail = root; 16 else 17 { 18 p = tmp->fail; //p 指向tmp 的失败指针 19 20 while (p != NULL) //p走到了root, (root->fail = NULL) 21 { 22 if (p->next[i] != NULL) //在失败节点的子节点中看是否存在相同的单词 23 { //若存在则将失败指针指向它,然后结束循环 24 tmp->next[i]->fail = p->next[i]; 25 break; 26 } 27 p = p->fail; //沿着失败指针找 28 } 29 if (p == NULL) //失败则指向root 30 tmp->next[i]->fail = root; 31 } 32 Q[head++] = tmp->next[i]; //进栈 33 } 34 35 } 36 37 } 38 }
- 首先是构建与根节点相连的节点,其节点的失败指针都是指向root的,即:节点root->s 和root->h ,因为代码14行, 则失败指针指向root
- 构建s->a 时, 不断的父亲节点的失败指针中找, 看是否存在字母‘a’(第一次:s->fail->next['a'] != NULL //第22行), 若存在则指向该节点指向该店(24行), 否则不断向上(父亲)的失败指针找 (p = p->fail)再在p->next[] 中找,一直循环到root 结束(root->fail = NULL) 循环到root , 即p = NULL 进入30行, 没有找到相同的字母那么指向root
- 构建s->h 时, 由于s->fail = root, root->next['h'] 存在(22行), 所以 24行,break.
- 构建h->e时, 由于h->fail = h, h->next[e] 存在,则24行
在上面我是列举了几个特别的节点,通过上面的几个构建可以知道,其失败指针的构建就是不断的在父亲节点的失败指针中看是否有匹配的,若没有,则再在失败的指针的失败的指针中找(有点绕。。)知道root根结束。
- 查询
1 int query(char *str, node *root) 2 { 3 int i=0, cnt = 0, index; 4 node *p = root; 5 while (str[i]) 6 { 7 index = str[i] - 'a'; 8 while (p->next[index] == NULL && p != root) //到达非根节点, 其next不存在该字符,则跳到fail继续匹配 9 p = p->fail; 10 p = p->next[index]; 11 if (p == NULL) //跳到NULL, 即root->fail 12 p = root; 13 node *tmp = p; 14 //在字母树里面找,是否存在以str[i] 为最后一位的字母的单词。 15 while (tmp != root) 16 { 17 cnt += tmp->flg; 18 tmp ->flg = 0; 19 tmp = tmp->fail; 20 } 21 i++; 22 } 23 return cnt; 24 }
在查询中很重要的一步就是失败指针的使用了。
从root 不断向下匹配,每次的匹配都需要查询下字典树的失败指针的flg
在字符串”yasherhs“ 中,root根没有ya,因此p 不做任何操作,然后‘s',’h','e', p = p->next[],并且在每一步中都需要查看一下p->flg 和p->fail的flg,并且将它们置为0, 以防止下次会重复计算(第15~第20行)。
若再匹配过程中出现p->next[] 不存在了,那么着需要利用到p->fail(第8行)
至此基本上我们就完成了AC自动机的基本操作。
HDU222完整代码如下:
1 #include <iostream> 2 #include <stdio.h> 3 using namespace std; 4 5 const int kind = 26; 6 const int M = 500001; 7 char str[1000005]; 8 //trie树节点 9 struct node 10 { 11 int flg; //标志位 12 node *fail; //失败指针 13 node *next[kind]; //子节点 14 node() //初始化节点 15 { 16 flg = 0; 17 fail = NULL; 18 memset(next, NULL, sizeof(next)); 19 } 20 }*root, *Q[M]; 21 22 //构建trie 树 23 void insert(char *str, node *root) 24 { 25 node *p = root; 26 int i=0, index; 27 while (str[i]) 28 { 29 index = str[i]-'a'; 30 if (p->next[index] == NULL) //若子节点没有则新建一个节点 31 p->next[index] = new node; 32 p = p->next[index]; //指向下一个点 33 i++; 34 } 35 p->flg++; //在单词的最后一个节点加1,以便在后面的查询中使用 36 } 37 38 void build_AC(node *root) 39 { 40 int head=0, tail=0, i; 41 root->fail = NULL; //根节点的失败指针位NULL 42 Q[head++] = root; //根节点入栈 43 while (head !=tail) 44 { 45 node *tmp = Q[tail++]; 46 node *p = NULL; 47 for (i=0; i<kind; i++) 48 { 49 if (tmp->next[i] != NULL) //若该单词存在则进入 50 { 51 if (tmp == root) //根节点的子节点,失败指针都指向root 52 tmp->next[i]->fail = root; 53 else 54 { 55 p = tmp->fail; //p 指向tmp 的失败指针 56 57 while (p != NULL) //p走到了root, (root->fail = NULL) 58 { 59 if (p->next[i] != NULL) //在失败节点的子节点中看是否存在相同的单词 60 { //若存在则将失败指针指向它,然后结束循环 61 tmp->next[i]->fail = p->next[i]; 62 break; 63 } 64 p = p->fail; //沿着失败指针找 65 } 66 if (p == NULL) //失败则指向root 67 tmp->next[i]->fail = root; 68 } 69 Q[head++] = tmp->next[i]; //进栈 70 } 71 72 } 73 74 } 75 } 76 77 int query(char *str, node *root) 78 { 79 int i=0, cnt = 0, index; 80 node *p = root; 81 while (str[i]) 82 { 83 index = str[i] - 'a'; 84 while (p->next[index] == NULL && p != root) //到达非根节点, 其next不存在该字符,则跳到fail继续匹配 85 p = p->fail; 86 p = p->next[index]; 87 if (p == NULL) //跳到NULL, 即root->fail 88 p = root; 89 node *tmp = p; 90 //在字母树里面找,是否存在以str[i] 为最后一位的字母的单词。 91 while (tmp != root) 92 { 93 cnt += tmp->flg; 94 tmp ->flg = 0; 95 tmp = tmp->fail; 96 } 97 i++; 98 } 99 return cnt; 100 } 101 102 int main() 103 { 104 int t, n; 105 scanf("%d", &t); 106 while (t--) 107 { 108 scanf("%d", &n); 109 getchar(); 110 node *root = new node; 111 while (n--) 112 { 113 gets(str); 114 insert(str, root); 115 } 116 build_AC(root); 117 gets(str); 118 printf("%d\n", query(str, root)); 119 } 120 return 0; 121 }