发现字符串处理方面的基础很薄弱,于是这两天打算加强一下,去做了一个AC自动机的题。在做AC自动机之前,先做了一个trie树的题练手,做好准备。
说起AC自动机,就让我想起了数字电路里面的状态图和状态转移。当初一道实验题目就是设计一个电路,当输入的一串0-1信号中出现了给定的模式时输出一个高电平,做法就是根据特定模式编程设计一个状态图,然后根据不同的输入在状态图上转移,在某些状态下输入特定值则输出高电平。
发现AC自动机跟这个有异曲同工之妙。AC自动机则是根据已知的单词得出单词树(即Trie树),然后在单词树上产生失配指针(即得到状态转移的指针),最后将文本作线性扫描得到结果。
当然AC自动机与数电实验还是有一定区别。详细解析见下文:
1.字典树Trie
建立一个字典树,作用在于压缩信息,更容易求得公共前缀。
2.失配指针
失配指针在于高效地更新公共前缀,利用其中的信息。由BFS的性质以及Trie树的性质可知,若某一序列s[1...m]在m+1处失配时,则该序列更新为s[i...m](i >= 2 && i <= m)。
3.AC自动机
之前自己写了一个AC自动机的主程序,结果是错的。后来在网上找到一段代码,仔细比较,终于发现了错误,并明白了AC自动机的原理。
对于一个Trie树,建立失配指针后,Trie树会具有一些特殊的性质:
首先声明几个重要的指针。
1)指针p。指向当前已匹配的字符。若p指向root,则当前匹配的字符序列为空。
2)指针p->fail。指向与p有相同字符的节点,即p的失配指针。
3)指针temp。
对于Trie树中的一个节点,对应一个序列s[1...m]。此时,p指向字符s[m]。若在下一个字符处失配,即p->next[s[m+1]] == NULL,则由失配指针跳到另一个节点(p->fail)处,该节点对应的序列为s[i...m]。若继续失配,则序列依次跳转直到序列为空或出现匹配。在此过程中,p的值一直在变化,但是p对应节点的字符没有发生变化。在此过程中,我们观察可知,最终求得得序列s则为最长公共后缀。另外,由于这个序列是从root开始到某一节点,则说明这个序列有可能是某些序列的前缀。
再次讨论p指针转移的意义。如果p指针在某一字符s[m+1]处失配(即p->next[s[m+1]] == NULL),则说明没有单词s[1...m+1]存在。此时,如果p的失配指针指向root,则说明当前序列的任意后缀不会是某个单词的前缀。如果p的失配指针不指向root,则说明序列s[i...m]是某一单词的前缀,于是跳转到p的失配指针,以s[i...m]为前缀继续匹配s[m+1]。
对于已经得到的序列s[1...m],由于s[i...m]可能是某单词的后缀,s[1...j]可能是某单词的前缀,所以s[1...m]中可能会出现单词。此时,p指向已匹配的字符,不能动。于是,令temp = p,然后依次测试s[1...m], s[i...m]是否是单词。
大致总结在这里吧。没有图片,看起来不是很方便。推荐blog:http://www.cppblog.com/mythit/archive/2009/04/21/80633.html。感觉该blog在AC自动机的运行原理上没有解释得特别清楚,于是写了这片随笔,算是互为补充吧。不足之处欢迎大家指出。最后,谢谢“AC自动机算法详解”的作者。如果有兴趣还可以看看《柔性字符串匹配》这本书。
另外,“AC自动机算法详解”这篇blog上面的代码有一处细微的错误,已经改正。
HDU 2222 Keywords Search
1 #include <stdio.h>
2 #include <string.h>
3
4 const int MaxN = 1000010;
5
6 struct Node {
7 int count;
8 struct Node *fail, *next[26];
9 }node[MaxN<<1];
10
11 typedef struct Node *NodePtr;
12
13 struct Trie {
14 NodePtr root;
15 int mtp;
16 void init() { //<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
17 root = node; mtp = 1;
18 memset(root, 0, sizeof(Node));
19 }
20 void insert(char *s) {
21 NodePtr p = root;
22 while(*s) {
23 if(!p->next[*s-97]) {
24 p->next[*s-97] = node+mtp++;
25 memset(p->next[*s-97], 0, sizeof(Node));
26 }
27 p = p->next[*s-97]; s++;
28 }
29 p->count++;
30 }
31 void fail_ptr(); //<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
32 int text_match(char *text); //<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
33 }trie;
34
35 NodePtr Q[MaxN];
36 int qs, qe;
37
38 void Trie::fail_ptr() {
39 NodePtr p, q;
40 Q[0] = root; qs = 0; qe = 1;
41 while(qs != qe) {
42 p = Q[qs++]; if(qs == MaxN) qs = 0;
43 q = NULL;
44 for(int i = 0; i < 26; ++i) if(p->next[i]) {
45 if(p == root) p->next[i]->fail = root;
46 else {
47 q = p->fail;
48 while(q != NULL) {
49 if(q->next[i] != NULL) {
50 p->next[i]->fail = q->next[i];
51 break;
52 }
53 q = q->fail;
54 }
55 if(q == NULL)
56 p->next[i]->fail = root;
57 }
58 Q[qe++] = p->next[i]; if(qe == MaxN) qe = 0;
59 }
60 }
61 }
62
63 int Trie::text_match(char *s) {
64 NodePtr p = root, temp;
65 int cnt = 0, i;
66 while(*s) {
67 i = *s-97;
68 while(p->next[i] == NULL && p != root) //最长后缀
69 p = p->fail;
70 p = (p->next[i] == NULL)? root : p->next[i];
71 temp = p;
72 while(temp != root){ //注意这段代码
73 cnt += temp->count;
74 temp->count = 0;
75 temp = temp->fail;
76 }
77 s++;
78 }
79 return cnt;
80 }
81
82 char word[55], text[MaxN];
83 int n;
84
85 int main() {
86 int t;
87 for(scanf("%d", &t);t--;) {
88 scanf("%d%*c", &n);
89 trie.init();
90 for(int i = 0; i < n; ++i) {
91 scanf("%s", word);
92 trie.insert(word);
93 }
94 trie.fail_ptr();
95 scanf("%s", text);
96 printf("%d\n", trie.text_match(text));
97 }
98 return 0;
99 }
100