AC自动机算法

AC自动机算法

分类: 算法总结   2609人阅读  评论(0)  收藏  举报

AC自动机简介: 

首先简要介绍一下AC自动机:Aho-Corasick automation,该算法在1975年产生于贝尔实验室,是著名的多模匹配算法之一。一个常见的例子就是给出n个单词,再给出一段包含m个字符的文章,让你找出有多少个单词在文章里出现过。要搞懂AC自动机,先得有字典树Trie和KMP模式匹配算法的基础知识。KMP算法是单模式串的字符匹配算法,AC自动机是多模式串的字符匹配算法。

AC自动机的构造:

1.构造一棵Trie,作为AC自动机的搜索数据结构。

2.构造fail指针,使当前字符失配时跳转到具有最长公共前后缀的字符继续匹配。如同 KMP算法一样, AC自动机在匹配时如果当前字符匹配失败,那么利用fail指针进行跳转。由此可知如果跳转,跳转后的串的前缀,必为跳转前的模式串的后缀并且跳转的新位置的深度(匹配字符个数)一定小于跳之前的节点。所以我们可以利用 bfs在 Trie上面进行 fail指针的求解。

3.扫描主串进行匹配。

AC自动机详讲:

我们给出5个单词,say,she,shr,he,her。给定字符串为yasherhs。问多少个单词在字符串中出现过。

一、Trie

首先我们需要建立一棵Trie。但是这棵Trie不是普通的Trie,而是带有一些特殊的性质。

首先会有3个重要的指针,分别为p, p->fail, temp。

1.指针p,指向当前匹配的字符。若p指向root,表示当前匹配的字符序列为空。(root是Trie入口,没有实际含义)。

2.指针p->fail,p的失败指针,指向与字符p相同的结点,若没有,则指向root。

3.指针temp,测试指针(自己命名的,容易理解!~),在建立fail指针时有寻找与p字符匹配的结点的作用,在扫描时作用最大,也最不好理解。

对于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]是否是单词。

构造的Trie为:


二、构造失败指针

用BFS来构造失败指针,与KMP算法相似的思想。

首先,root入队,第1次循环时处理与root相连的字符,也就是各个单词的第一个字符h和s,因为第一个字符不匹配需要重新匹配,所以第一个字符都指向root(root是Trie入口,没有实际含义)失败指针的指向对应下图中的(1),(2)两条虚线;第2次进入循环后,从队列中先弹出h,接下来p指向h节点的fail指针指向的节点,也就是root;p=p->fail也就是p=NULL说明匹配序列为空,则把节点e的fail指针指向root表示没有匹配序列,对应图-2中的(3),然后节点e进入队列;第3次循环时,弹出的第一个节点a的操作与上一步操作的节点e相同,把a的fail指针指向root,对应图-2中的(4),并入队;第4次进入循环时,弹出节点h(图中左边那个),这时操作略有不同。由于p->next[i]!=NULL(root有h这个儿子节点,图中右边那个),这样便把左边那个h节点的失败指针指向右边那个root的儿子节点h,对应图-2中的(5),然后h入队。以此类推:在循环结束后,所有的失败指针就是图-2中的这种形式。


三、扫描

构造好Trie和失败指针后,我们就可以对主串进行扫描了。这个过程和KMP算法很类似,但是也有一定的区别,主要是因为AC自动机处理的是多串模式,需要防止遗漏某个单词,所以引入temp指针。

匹配过程分两种情况:(1)当前字符匹配,表示从当前节点沿着树边有一条路径可以到达目标字符,此时只需沿该路径走向下一个节点继续匹配即可,目标字符串指针移向下个字符继续匹配;(2)当前字符不匹配,则去当前节点失败指针所指向的字符继续匹配,匹配过程随着指针指向root结束。重复这2个过程中的任意一个,直到模式串走到结尾为止。

 对照上图,看一下模式匹配这个详细的流程,其中模式串为yasherhs。对于i=0,1。Trie中没有对应的路径,故不做任何操作;i=2,3,4时,指针p走到左下节点e。因为节点e的count信息为1,所以cnt+1,并且讲节点e的count值设置为-1,表示改单词已经出现过了,防止重复计数,最后temp指向e节点的失败指针所指向的节点继续查找,以此类推,最后temp指向root,退出while循环,这个过程中count增加了2。表示找到了2个单词she和he。当i=5时,程序进入第5行,p指向其失败指针的节点,也就是右边那个e节点,随后在第6行指向r节点,r节点的count值为1,从而count+1,循环直到temp指向root为止。最后i=6,7时,找不到任何匹配,匹配过程结束。


到此,AC自动机入门知识就讲完了。HDU 2222入门题必须果断A掉,反正我是参考别人代码敲的。。。

AC自动机貌似还有很多需要优化的地方,等把基础搞定之后再学习一下怎么优化吧。。



AC自动机

关键字:AC自动机 自动机 有限状态自动机 Trie 字母树 字符串匹配 多串匹配算法

Note:阅读本文需要有KMP算法基础,如果你不知道什么是KMP,请看这里:

http://www.matrix67.com/blog/article.asp?id=146   (Matrix67大牛写的)

AC自动机是用来处理多串匹配问题的,即给你很多串,再给你一篇文章,让你在文章中找这些串是否出现过,在哪出现。也许你考虑过AC自动机名字的含义,我也有过同样的想法。你现在已经知道KMP了,他之所以叫做KMP,是因为这个算法是由Knuth、Morris、Pratt三个提出来的,取了这三个人的名字的头一个字母。那么AC自动机也是同样的,他是Aho-Corasick。所以不要再YY地认为AC自动机是AC(cept)自动机,虽然他确实能帮你AC一点题目。

。。。扯远了。。。

要学会AC自动机,我们必须知道什么是Trie,即字母树。如果你会了,请跳过这一段

        Trie是由字母组成的。

       先看张图: 

这就是一棵Trie树。用绿色标出的点表示一个单词的末尾(为什么这样表示?看下去就知道了)。树上一条从root到绿色节点的路径上的字母,组成了一个“单词”。

       /* 也许你看了这一段,就知道如何构建Trie了,那请跳过以下几段。*/

        那么如何来构建一棵Trie呢?就让我从一棵空树开始,一步步来构建他。

一开始,我们有一个root:

现在,插入第一个单词,she。这就相当于在树中插入一条链。过程很简单。插完以后,我们在最后一个字母’e’上加一个绿色标记,结果如图:

        再来一个单词,shr(什么词?…..右位移啊)。由于root下已经有’s’了,我们就不重复插入了,同理,由于’s’下有’h’了,我们也略过他,直接在’h’下插入’r’,并把’r’标为绿色。结果如图:

       按同样的方法,我们继续把余下的元素插进树中。

       最后结果:

       

     也就是这样:

      

好了,现在我们已经有一棵Trie了,但这还不够,我们还要在Trie上引入一个很强大的东西:失败指针或者说shift数组或者说Next函数 …..你爱怎么叫怎么叫吧,反正就是KMP的精华所在,这也是我为什么叫你看KMP的原因。

KMP中我们用两个指针i和j分别表示,A[i-j+ 1..i]与B[1..j]完全相等。也就是说,i是不断增加的,随着i的增加j相应地变化,且j满足以A[i]结尾的长度为j的字符串正好匹配B串的前 j个字符,当A[i+1]<>B[j+1],KMP的策略是调整j的位置(减小j值)使得A[i-j+1..i]与B[1..j]保持匹配且新的B[j+1]恰好与A[i+1]匹配(从而使得i和j能继续增加)。

Trie树上的失败指针与此类似。

        假设有一个节点k,他的失败指针指向j。那么k,j满足这个性质:设root到j的距离为n,则从k之上的第n个节点到k所组成的长度为n的单词,与从root到j所组成的单词相同。

        比如图中she中的’e’的失败指针就应该指向her中的’e’。因为:

     

图中红框部分是完全一样的。

那么我们要怎样构建这个东西呢?其实我们可以用一个简单的BFS搞定这一切。

对于每个节点,我们可以这样处理:设这个节点上的字母为C,沿着他父亲的失败指针走,直到走到一个节点,他的儿子中也有字母为C的节点。然后把当前节点的失败指针指向那个字目也为C的儿子。如果一直走到了root都没找到,那就把失败指针指向root

最开始,我们把root加入队列(root的失败指针显然指向自己),这以后我们每处理一个点,就把它的所有儿子加入队列,直到搞完。

至于为什么这样就搞的定,我们讲下去就知道了。

好了,现在我们有了一棵带失败指针的Trie了,而我的文章也破千字了,接下来,我们就要讲AC自动机是怎么工作的了。

AC自动机是个多串匹配,也就是说会有很多串让你查找,我们先把这些串弄成一棵Trie,再搞一下失败指针,然后我们就可以开始AC自动机了。

一开始,Trie中有一个指针t1指向root,待匹配串(也就是“文章”)中有一个指针t2指向串头。

接下来的操作和KMP很相似:如果t2指向的字母,是Trie树中,t1指向的节点的儿子,那么t2+1,t1改为那个儿子的编号,否则t1顺这当前节点的失败指针向上找,直到t2是t1的一个儿子,或者t1指向根。如果t1路过了一个绿色的点,那么以这个点结尾的单词就算出现过了。或者如果t1所在的点可以顺着失败指针走到一个绿色点,那么以那个绿点结尾的单词就算出现过了。

我们现在回过来讲讲失败指针。实际上找失败指针的过程,是一个自我匹配的过程。

如图,现在假定我们确定了深度小于2(root深度为1)的所有点的失败指针,现在要确定e。这就相当于我们有了这样一颗Trie:

而文章为’she’,要查找’e’在哪里出现。我们接着匹配’say’,那’y’的失败指针就确定了。

好好想想。前面讲的BFS其实就是自我匹配的过程,这也是和KMP很相似的。

好了,就写到这吧,有不明白可以留言或发邮件给我(drdarkraven@gmail.com),或者在推上fo我(@sdraven)....

                              

                              

DarkRaven原创

做人要厚道,转载请注明出处(否则你将中AC自动机的诅咒,永远A不了题~)

AC自动机

Filed under:  Article, Blablabla..., My ACM-ICPC Career, Work —   — OWenT @ 下午 8:10

某个课程的作业,促使我来看看这玩意。
整个程序的算法思想是看别人的ACM的blog看懂的,感觉确实和KMP很像。但是代码呢就比较工程化一点。顺便回忆了一把ACM的感觉。
基本原理呢基于字典树,并增加了失败节点。
实现原理类似KMP算法,但是一次可以匹配多个字符串。在匹配失败时转向失败节点,并从失败节点开始继续向下匹配。
比如:我们有字典集合
acd、aceb、bef、cef
节点关系如图所示,黑色箭头为失败指针

当查找acefcab时,首先会按aceb的支路一直匹配到e,在e的位置发现找不到f,然后跳转到e的失败节点(即cef支路的e节点),查到f。并以此完成了第一次匹配。
接下来从根节点重新匹配并分别进入第一层的c节点,回到根节点,进入a节点,回到根节点,和进入b节点。
并在最终只匹配成功了cef

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
/**
  * AC 自动机, 数节点类和自动机功能类
  * 文档格式:doxygen
  * @author owentou, owt5008137@live.com
  * @date 2012.08.28
  */
 
#ifndef __AC_AUTOMATION_HPP_
#define __AC_AUTOMATION_HPP_
 
#if defined(_MSC_VER) && (_MSC_VER >= 1020)
# pragma once
#endif
 
#include <map>
#include <string>
#include <cstddef>
#include <list>
#include <vector>
#include <assert.h>
#include "smart_ptr.h"
 
 
template < typename CH = char >
class ACTrie: public std::enable_shared_from_this< ACTrie<CH> >
{
public :
     typedef std::shared_ptr< ACTrie<CH> > ptr_type;
 
private :
     /**
      * 关联的匹配字符串<br />
      * size不为0表示该节点有关联的字符串并且是最后一个节点
      */
     std::string m_strMatchedString;
 
     /**
      * 失败转向节点
      */
     ptr_type m_pFailed;
 
     /**
      * 下一个查找项
      */
     std::map<CH, ptr_type> m_stNext;
 
     /**
      * 初始化自身和子节点的失败指针
      * @param pPreFailed 初始搜索的指针(一般为父节点的失败指针)
      * @param cChar 搜索的字符
      */
     void _init_failed(ptr_type pPreFailed, const CH& cChar)
     {
         typedef typename std::map<CH, ptr_type>::iterator iter_type;
 
         // 设置自身的失败指针
         iter_type iter;
         for (;; pPreFailed = pPreFailed->m_pFailed)
         {
             iter = pPreFailed->m_stNext.find(cChar);
             if (iter != pPreFailed->m_stNext.end())
             {
                 m_pFailed = iter->second;
                 break ;
             }
 
             if (NULL == pPreFailed->m_pFailed.get())
             {
                 m_pFailed = pPreFailed;
                 break ;
             }
         }
     }
 
     /**
      * 把子节点填充到链表中(用于BFS)<br />
      * 调用此函数时,当前节点的失败指针必须已经设置好
      * @param stList 填充目标
      */
     void _fill_children(std::list< std::pair<CH, ptr_type> >& stList)
     {
         typedef typename std::map<CH, ptr_type>::iterator iter_type;
         for (iter_type iter = m_stNext.begin();
             iter != m_stNext.end();
             ++ iter)
         {
             iter->second->m_pFailed = m_pFailed;    // 临时用于记录父节点的失败指针
             stList.push_back(std::make_pair(iter->first, iter->second));
         }
     }
 
     /**
      * 获取当前指针
      * @return 当前对象的智能指针
      */
     ptr_type _get_ptr()
     {
         return this ->shared_from_this();
     }
public :
     ACTrie(ptr_type pRoot): m_pFailed(pRoot){}
 
     /**
      * 设置失败指针
      * @param pFailed 失败指针
      */
     void SetFailed(ptr_type pFailed)
     {
         m_pFailed = pFailed;
     }
 
     /**
      * 初始化根节点中,子节点的失败指针<br />
      * 当前节点会被视为根节点
      */
     void InitFailed()
     {
         m_pFailed = ptr_type(NULL);
         std::list< std::pair<CH, ptr_type> > stList;
 
         typedef typename std::map<CH, ptr_type>::iterator iter_type;
 
         // 第一层节点
         for (iter_type iter = m_stNext.begin();
             iter != m_stNext.end();
             ++ iter)
         {
             iter->second->m_pFailed = _get_ptr();
             iter->second->_fill_children(stList);
         }
 
         // 后续节点 BFS 建树
         while (stList.size() > 0)
         {
             std::pair<CH, ptr_type> stNode = stList.front();
             stList.pop_front();
             stNode.second->_init_failed(stNode.second->m_pFailed, stNode.first);
             stNode.second->_fill_children(stList);
         }
     }
 
     /**
      * 清空后续分支
      */
     void Reset()
     {
         m_stNext.clear();
     }
 
     /**
      * 当前节点是否是一个关键字的最后一个节点
      * @return 如果是返回true
      */
     bool IsLastNode() const
     {
         return m_strMatchedString.size() > 0;
     }
 
     /**
      * 构建关键字的字典树节点
      * @param pStr          当前字符指针
      * @param iLeftBytes    关键字剩余字节数
      * @param strOrigin     关键字原始内容
      */
     void InsertChildren( const CH* pStr, int iLeftBytes, const std::string& strOrigin)
     {
         // 最后一个节点
         if (0 >= iLeftBytes)
         {
             m_strMatchedString.assign(strOrigin.data(), strOrigin.size());
             return ;
         }
 
         iLeftBytes -= sizeof (CH);
 
         typedef typename std::map<CH, ptr_type>::iterator iter_type;
         iter_type iter = m_stNext.find(*pStr);
         if (iter != m_stNext.end())
         {
             iter->second->InsertChildren(pStr + 1, iLeftBytes, strOrigin);
             return ;
         }
 
         std::pair<iter_type, bool > iter_new = m_stNext.insert(std::make_pair(*pStr, ptr_type( new ACTrie<CH>(m_pFailed))));
         assert (iter_new.second);
 
         iter_new.first->second->InsertChildren(pStr + 1, iLeftBytes, strOrigin);
     }
 
     /**
      * 匹配目标字符
      * @param pChar 目标字符指针
      * @param iLeftBytes 剩余字节数
      * @return 第一项为匹配完成后剩余字节数,第二项为匹配的关键字<br />
      *         如果匹配失败,第一项为0或负数,第二项为空串
      */
     std::pair< int , std::string> Match( const CH* pChar, int iLeftBytes) const
     {
         using namespace std;
         // 成功匹配
         if (IsLastNode())
         {
             return std::make_pair(iLeftBytes, m_strMatchedString);
         }
 
         // 已到目标串目末尾,无匹配
         if (iLeftBytes <= 0)
         {
             return std::make_pair(iLeftBytes, std::string( "" ));
         }
 
         // 匹配下一项
         typedef typename std::map<CH, ptr_type>::const_iterator iter_type;
         iter_type iter = m_stNext.find(*pChar);
         if (iter != m_stNext.end())
         {
             return iter->second->Match(pChar + 1, iLeftBytes - sizeof (CH));
         }
         // 如果是root节点,往后匹配
         if (NULL == m_pFailed->m_pFailed.get())
         {
             return Match(pChar + 1, iLeftBytes - sizeof (CH));
         }
         // 否则, failed节点进行匹配
         return m_pFailed->Match(pChar, iLeftBytes);
     }
};
 
template < typename CH = char >
class ACAutomation
{
public :
     typedef typename ACTrie<CH>::ptr_type trie_type;
     typedef std::pair< size_t , const std::string> item_type;
     typedef std::vector< item_type > value_type;
 
private :
     /**
      * 根节点(空节点)
      */
     std::shared_ptr< ACTrie<CH> > m_pRoot;
 
     bool m_bIsInited;
 
     /**
      * 初始化字典树的失败指针
      */
     void init()
     {
         if (m_bIsInited)
             return ;
 
         m_pRoot->InitFailed();
 
         m_bIsInited = true ;
     }
 
public :
     ACAutomation():
         m_pRoot( new ACTrie<CH>( std::shared_ptr< ACTrie<CH> >(NULL) )),
         m_bIsInited( false )
     {
         // 临时的自环
         m_pRoot->SetFailed(m_pRoot);
     }
 
     ~ACAutomation()
     {
         // 解除自环,防止内存泄漏
         m_pRoot->SetFailed(std::shared_ptr< ACTrie<CH> >(NULL));
     }
 
     /**
      * 增加关键字
      * @param strKeyword 关键字字符串
      */
     void InsertKeyword( const std::string& strKeyword)
     {
         assert (strKeyword.size() > 0);
 
         m_bIsInited = false ;
         m_pRoot->InsertChildren( static_cast < const CH*>(strKeyword.c_str()), strKeyword.size(), strKeyword);
     }
 
     /**
      * 匹配目标串,返回匹配结果
      * @param strContent 目标字符串
      * @return 返回的结果列表,返回结果的first为开始位置,second为匹配的关键字
      */
     value_type Match( const std::string& strContent)
     {
         using std:: size_t ;
         init();
         using namespace std;
         value_type ret;
         int iSize = static_cast < int >(strContent.size()), iLeft = iSize;
         const char * pEnd = strContent.data() + iSize;
 
         while (iLeft > 0)
         {
             pair< int , string> res = m_pRoot->Match( static_cast < const CH*>(pEnd - iLeft), iLeft);
             iLeft = res.first;
             if (res.second.size() > 0)
             {
                 ret.push_back(std::make_pair( static_cast < size_t >(iSize - iLeft) - res.second.size(),
                     res.second));
             }
         }
 
         return ret;
     }
 
     /**
      * 清空关键字列表
      */
     void Reset()
     {
         m_pRoot->Reset();
     }
};
 
#endif

其中的 smart_ptr.h 文件见 http://www.owent.net/?p=643
注意:这段代码没经过边界条件测试、压力测试 等等各种测试,所以不是稳定版
接下来是测试使用的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
  * AC 自动机, 关键字过滤作业,匹配逻辑<br />
  * VC 11        中编译测试通过[Windows]
  * GCC 4.7.1    中编译测试通过[Linux]  (with -std=c++11 -lstdc++)
  * 文档格式:doxygen
  * @author owentou, owt5008137@live.com
  * @date 2012.08.25
  */
 
#include <iostream>
#include "ACAutomation.hpp"
 
 
int main()
{
     ACAutomation<> stAcTree;
 
     stAcTree.InsertKeyword( "acd" );
     stAcTree.InsertKeyword( "aceb" );
     stAcTree.InsertKeyword( "bef" );
     stAcTree.InsertKeyword( "cef" );
 
     ACAutomation<>::value_type stRes = stAcTree.Match( "acefcab" );
 
     for (auto stItem: stRes)
     {
         std::cout<< "Position: " << stItem.first<< " Matched Keyword: " << stItem.second<< std::endl;
     }
 
     return 0;
}

如注释所言,4.7.0 以前的GCC 就不用争扎了,编译不过的

以下内容包含了完整对AC自动机的解释构建过程
 

  • 分享到:
The short URL of the present article is:  http://www.owent.net/WtiNY

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值