ac算法基础知识


本文转载自一叶菩提 《AC算法原理详解--c语言实现》
近来两天在学习AC算法,现在来做个总结。也希望能在将来的某一天发现现在的我是多么的幼稚。当然如果能帮助哪位朋友,也能像我今天这样幼稚的理解了一下AC算法,然后再去走向成熟,那就不胜荣幸了。

先来个链接,看看人家比较 专业的解释http://qqff88.blog.hexun.com/8685520_d.html

再来个 算法的动态演示http://artlives.ru/ahocorasik/en/

接下来就是个人浅薄的理解了:
首先说AC算法和KMP算法的比较
前两天提到的 KMP算法是一个单模匹配,AC算法是一种多模匹配的方法。
kmp算法比普通的字符比较方法的优点在于目标串不需要回溯,模式串回溯的距离尽可能小。
ac算法比kmp的优点在于不但目标串不需要回溯,连模式串都不用回溯了。(多个模式串,串在一起来匹配)不明白没关系,继续看。

接着说 AC算法的理论
先说 构成:AC算法的整个计算过程需要三个东西: 模式树、失效函数、输出函数。可能比较专业的文章里,对这三者得解释很术语化,不方便自学的人理解。先对它们做个简单的说明吧: 模式树就是说根据若干个模式串建立一棵树。例如:有模式串
abc, acb, bca, cab, a 。
AC算法原理详解--c语言实现 - 一叶菩提 - 一叶菩提
图 1
两个圈表示该节点具有结束状态,就是某一个串的最后一个字符形成的状态。

失效函数:这个函数和后边的输出函数,说的是数学里的函数的概念,不是编程里的函数的概念。
失效函数是指如果当前节点在匹配的时候失败了,它应该跳转到哪个节点去继续匹配呢。当然,我们可能会想,某个模式串匹配失败,说明当前模式串在目标串中不存在呗,我们应该继续匹配下一个模式串啊。对,的确是应该匹配下一个模式串,但是我们最好去匹配哪个呢?为什么呢?这就是失效函数的意义所在了。
例如:我们在匹配 abc 这个模式串,当 a b 都匹配成功了,最后 c 匹配失败了。 这时我们需要去匹配另一个模式串,这时还有 acb  bca cab  a 这四个模式串,我们接下来匹配哪个最好呢。即使你不知道,它也应该是 bca , 为什么?因为 此时 b 已经匹配成功了,这四个待匹配的串中只有 bca 是b开头的啊,就是说我们选择 这个串可以利用先前就匹配过的一个字符。这样匹配成功的字符就不用被浪费了。(现在对kmp算法里那个模式串字符值数组的意义是不是有了新的认识了)。个人觉得,这就是AC比kmp先进的地方了,kmp 节省了模式串自身的回溯距离,说白了就是已经匹配成功的部分有可以利用的就利用上。AC把各个模式串串连起来,同样实现了这种有效利用。这些就是失效函数的意义了。那失效函数到底是个什么形象呢?
失效函数是某个节点到另外一个节点的跳转。
AC算法原理详解--c语言实现 - 一叶菩提 - 一叶菩提
图 2
看到了吗?这就是失效函数在树中的存在了,乱吧?没关系,它很乖。不懂也没关系,先看个大概。

继续, 输出函数:输出函数就是为了在匹配成功的时候能够告知程序现在匹配某个串成功了,这个串的内容是什么
给个形象:
AC算法原理详解--c语言实现 - 一叶菩提 - 一叶菩提    AC算法原理详解--c语言实现 - 一叶菩提 - 一叶菩提
图 3 图 4
这就是输出函数了,两个图是同一个函数,因为它是发展的,所以会发现 6和 8 两个节点的输出函数值不一样了。这是因为:输出函数是在建树和建立失效函数的过程中发展形成的。在建树的过程中只对有结束状态的节点确定输出函数的值。在建立失效函数的过程中会对某一个以结束状态的节点做为失效跳转状态的节点确定输出函数的值。例如,模式串 a 使得 节点 1 成为了结束状态的节点,后来 节点 8 的失效函数指向了节点1,所以呢,节点8的输出函数值就多了节点1的输出函数值。
以上这些就是AC算法的构成了 ,先给您形成一个概念。接下来讲讲 AC的计算过程

先从 建树说起:(节点* == 状态*)
建树的过程是从一个初始状态开始的,就是图1 最上边那个标记为0的节点。然后把每个模式串的每一个字符叫做一个动作,模式串abc 分为动作a,动作b,动作c。在某一个状态下做一个动作会形成另一个状态。(这个不难理解吧,你处在饿的状态做一个吃的动作就进入了饱的状态。)然后呢,当一个模式串的所有动作都做完之后形成的最后一个状态标记为结束状态。下一个模式串的动作从初始状态开始做。做动作的时候如果发现当前模式串的第一个动作,初始状态已经做过,就不用再做直接进入那个状态。例如模式串 a 。它的第一个动作 a 在模式串abc中已经作为第一个动作做过了,此时直接进入这个状态即可。如果某个模式串的动作是顺着其它模式串的动作直接进入了人家的状态,这时在人家的某个状态时自己到了结束状态,就要标记当前节点为结束节点。就像模式串a,顺着abc的动作进入了1的状态,此时自己结束了,就要标记状态1为结束状态。
建树的过程中要建立输出函数。这个函数建立很简单,就是对每个状态节点映射一个字符串做为函数结果。比如:状态3,它是模式串abc的结束状态,它的输出函数的结果就是 abc 三个字符。状态1 作为模式串a的结束状态,它的输出函数的结果值就是a一个字符。就是状态2,只是一个普通的状态,不作为结束状态,暂时保留输出函数值为空。

再说 失效函数
失效函数是指在匹配过程中,如果当前节点匹配失败了,应该去跳转到哪个节点去匹配,失效函数的值其实就是那个被跳转到的状态。那么它怎么建立呢?
失效函数的建立是分层来建立的(树的每个节点会根据其深度分层这都懂吧),初始状态的失效函数指向自己(??不要问我为什么)。第一层的节点失效函数都指向根节点(初始状态)。其它层得每个状态的失效指针都指向前一个状态的失效状态做过当前状态的动作形成的状态。

(失效指针的查找过程分为以下三种情况:)
1、每个状态的失效指针都指向前一个状态的失效状态做过当前状态的动作形成的状态。
2、如果它的前一个状态的失效状态也没有做过这个动作就再回溯一层失效状态。
3、如果在第二层都没有找到失效状态,则该节点的失效状态指向根节点。(就是说一层一层回溯,直到回溯到了根节点,结果根节点也没有做过相同的动作,即可使当前新节点的失效指针指向根节点)

例如:图2 的状态2,它的前一个动作是状态1, 状态1的失效状态是初始状态,状态2是做b动作产生的,初始状态也做过b动作,形成了状态4,所以2的失效状态就是4。再比如状态11,它的前一个动作是状态10,状态10的失效状态是状态7,11是由动作b产生的,但是7没有动作b,这是回溯到状态7的失效状态--初始状态,初始状态做过b的动作,形成了状态4,所以11的失效状态是4。
建立失效函数的过程中也要建立输出函数,确切的说是补充输出函数。就是前面提到的,如果某个状态的失效状态具有结束标志,那这个状态的输出函数值要添加那个状态的函数值。这个状态的标志我叫做”被结束“。如果某一个状态的失效状态具有”被结束“的标志,那这个状态的输出函数值也要天剑这个被结束状态的函数值。形成了8 和 6 那样的输出函数。

最后提一下 输出函数
输出函数的建立是在前两个过程中形成的。它的真正意义在于匹配过程中,到了结束 、 ”被结束“的状态时,记得返回匹配成功的结果,然后再继续。不同于失效函数的意义,失效函数的意义在于匹配过程中的跳转。

本文使用指南
1、第一遍看懵了,去看看演示过程,再看文章。
2、看了演示过程不明白或者看了文章还不明白,结合演示过程和文章反复看。体会那个过程。
3、虽然网上有实现的源码,但是个人以为不懂原理看源码也。。。
4、联系我,留言吧。

可能语言不够简练,望各位路人留言批评指正。鄙人才疏学浅,如有疏漏之处,还望各位 多多指教。另外,个人已经用c语言对本算法进行了实现,尚在测试阶段。主要针对单字节字符和双字节字符进行匹配搜索。不敢献丑于公众,如果需要可以留言索要,共同探讨学习。
 
 不知道这篇文章是不是对大家有帮助,请大家点评一下哪些方面需要改进,或者没看懂什么地方。谢谢!



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自动机貌似还有很多需要优化的地方,等把基础搞定之后再学习一下怎么优化吧。。


  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
很高兴回答你关于数据结构和算法的问题!在Java中,有许多基础的数据结构和算法可以使用。下面是一些常见的数据结构和算法的介绍: 1. 数组(Array):数组是一种线性数据结构,可以存储相同类型的元素。它们在内存中是连续存储的,并且可以通过索引来访问元素。 2. 链表(Linked List):链表是另一种常见的线性数据结构,它由节点组成,每个节点都包含一个数据项和一个指向下一个节点的指针。 3. 栈(Stack):栈是一种后进先出(LIFO)的数据结构。它支持两个基本操作:push(将元素放入栈顶)和pop(从栈顶删除元素)。 4. 队列(Queue):队列是一种先进先出(FIFO)的数据结构。它支持两个基本操作:enqueue(将元素放入队尾)和dequeue(从队头删除元素)。 5. 树(Tree):树是一种非线性数据结构,它由节点和边组成。每个节点可以有零个或多个子节点。常见的树包括二叉树、二叉搜索树和平衡二叉树。 6. 图(Graph):图是由节点和边组成的非线性数据结构。节点表示对象,边表示节点之间的关系。图可以是有向的或无向的。 在Java中,还有一些常见的算法,包括但不限于以下几种: 1. 排序算法:例如冒泡排序、选择排序、插入排序、快速排序和归并排序等。 2. 查找算法:例如线性查找和二分查找等。 3. 图算法:例如广度优先搜索(BFS)和深度优先搜索(DFS)等。 4. 动态规划:一种通过将问题分解成子问题来解决复杂问题的算法。 这只是数据结构和算法的一小部分基础知识,希望对你有所帮助!如果你还有其他问题,请随时提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值