发现字符串处理方面的基础很薄弱,于是这两天打算加强一下,去做了一个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