1. 简介

AC自动机是一种多模匹配的文本匹配算法。

如果采用naive的方法,即依次比较文本串s中是否包含模式串p1, p2,...非常耗时。考虑到这些模式串中可能具有相同子串,可以利用已经比较过的那些模式串的一些信息,来优化效率。容易想到的一种方法是为这些模式串构建一个trie树,可以较好的利用模式串的公共前缀信息。

但是如果只是采用普通的trie树,仍有 如果一个模式串P1不匹配,就要重新回到根节点再找下一个模式串P2,也就是对于下一个模式串P2,要从P2的起始元素开始,依次与文本串S比较。这同样不够高效。P2如果可以利用和P1的一些共性信息,使得可以从P2的尽可能靠后的元素开始,与文本串S比较,那么算法的时间复杂度可能会有效降低。

AC自动机采用了KMP算法找next的思路,为trie树中每个节点找fail节点。

KMP中next与AC中fail的区别:

KMP算法 中 next[j] = k 表示前缀 [0 ~ (k - 1)] 与 后缀 [(j - k) ~ (j - 1)] 这k个元素是对应相等的,但是P[j] 和 P[k] 是不一定相等要在下一次进行比较的;

AC自动机中则是 cur = cur.fail,也即从 [根节点 ~ cur.fail (包含cur.fail节点)] 这个前缀,与 [cur节点所在路径上某节点 ~ cur节点(包含cur节点)] 这个后缀,对应相等。

2. 算法原理

(转自参考链接1)

2.1 初识AC自动机

AC自动机的基础是Trie树。和Trie树不同的是,树中的每个结点除了有指向孩子的指针(或者说引用),还有一个fail指针,它表示输入的字符与当前结点的所有孩子结点都不匹配时(注意,不是和该结点本身不匹配),自动机的状态应转移到的状态(或者说应该转移到的结点)。fail指针的功能可以类比于KMP算法中next数组的功能。

我们现在来看一个用目标字符串集合{abd,abdk, abchijn, chnit, ijabdf, ijaij}构造出来的AC自动机

上图是一个构建好的AC自动机,其中根结点不存储任何字符,根结点的fail指针为null。虚线表示该结点的fail指针的指向,所有表示字符串的最后一个字符的结点外部都用红圈表示,我们称该结点为这个字符串的终结结点。每个结点实际上都有fail指针,但为了表示方便,本文约定一个原则,即所有指向根结点的 fail虚线都未画出

从上图中的AC自动机,我们可以看出一个重要的性质:每个结点的fail指针表示由根结点到该结点所组成的字符序列的所有后缀 和 整个目标字符串集合(也就是整个Trie树)中的所有前缀 两者中最长公共的部分

比如图中,由根结点到目标字符串“ijabdf”中的 ‘d’组成的字符序列“ijabd”的所有后缀在整个目标字符串集{abd,abdk, abchijn, chnit, ijabdf, ijaij}的所有前缀中最长公共的部分就是abd,而图中d结点(字符串“ijabdf”中的这个d)的fail正是指向了字符序列abd的最后一个字符。

2.2 AC自动机的运行过程

1表示当前结点的指针指向AC自动机的根结点,即curr = root

2从文本串中读取(下)一个字符

3当前结点的所有孩子结点中寻找与该字符匹配的结点,

若成功:判断当前结点以及当前结点fail指向的结点是否表示一个字符串的结束,若是,则将文本串中索引起点记录在对应字符串保存结果集合中(索引起点= 当前索引-字符串长度+1)。curr指向该孩子结点,继续执行第2步

若失败:执行第4步。

4若fail == null(说明目标字符串中没有任何字符串是输入字符串的前缀,相当于重启状态机)curr = root, 执行步骤2,

否则,将当前结点的指针指向fail结点,执行步骤3)

2.3  例子

来一个具体的例子加深理解,初始时当前结点为root结点,我们现在假设文本串text = “abchnijabdfk”。

图中的紫色实曲线表示了整个搜索过程中的当前结点指针的转移过程,结点旁的文字表示了当前结点下读取的文本串字符。比如初始时,当前指针指向根结点时,输入字符‘a’,则当前指针指向结点a,此时再输入字符‘b’,自动机状态转移到结点b,……,以此类推。图中AC自动机的最后状态只是恰好回到根结点,并不一定都会回到根节点。

需要说明的是,当指针位于结点b(图中曲线经过了两次b,这里指第二次的b,即目标字符串“ijabdf”中的b),这时读取文本串字符下标为9的字符(即‘d’)时,由于b的所有孩子结点(这里恰好只有一个孩子结点)中存在能够匹配输入字符d的结点,那么当前结点指针就指向了结点d,而此时该结点d的fail指针指向的结点又恰好表示了字符串“abc”的终结结点(用红圈表示),所以我们找到了目标字符串“abc”一次。这个过程我们在图中用虚线表示,但状态没有转移到“abd”中的d结点。

在输入完所有文本串字符后,我们在文本串中找到了目标字符串集合中的abd一次,位于文本串中下标为7的位置;目标字符串ijabdf一次,位于文本串中下标为5的位置。

3. 构造AC自动机的方法与原理

首先我们将所有的目标字符串插入到Trie树中然后通过广度优先遍历为每个结点的所有孩子节点的fail指针找到正确的指向

确定fail指针指向的问题和KMP算法中构造next数组的方式如出一辙。具体方法如下

1)将根结点的所有孩子结点的fail指向根结点,然后将根结点的所有孩子结点依次入列。

2)若队列不为空:

2.1)出列,我们将出列的结点记为curr, failTo表示curr的fail指向的结点,即failTo = curr.fail

2.2) a.判断curr.child[i] == failTo.child[i]是否成立,

成立:curr.child[i].fail = failTo.child[i],

不成立:判断 failTo == null是否成立

成立: curr.child[i].fail == root

不成立:执行failTo = failTo.fail,继续执行2.2)

b.curr.child[i]入列,再次执行再次执行步骤2)

若队列为空:结束

4. 代码实现

 #coding:utf-8
import queue class Node(object):
def __init__(self):
self.children = {}
self.fail = None
self.isWord = False
self.word = "" class ACAutomation(object):
""" AC Automation
"""
def __init__(self):
self.root = Node() def add(self, word):
cur_node = self.root
for char in word:
if char not in cur_node.children:
cur_node.children[char] = Node()
cur_node = cur_node.children[char]
cur_node.isWord = True
cur_node.word = word def link_fail(self):
que = queue.Queue()
que.put(self.root) while que.empty() == False: cur_node = que.get()
cur_fail = cur_node.fail for child_key, child_value in cur_node.children.items(): while True:
if cur_fail is None:
cur_node.children[child_key].fail = self.root
break elif child_key in cur_fail.children:
cur_node.children[child_key].fail = cur_fail.children[child_key]
break else:
cur_fail = cur_fail.fail que.put(cur_node.children[child_key]) def curWords(self, cur_node):
""" 该函数为查找当前节点处所有可能的匹配的模式串的集合
Args:
cur_node 当前节点
Returns:
set 当前节点处所有可能的匹配的模式串的集合 匹配成功模式串有两种情况:
1. 当前节点处 isWord = True, 则匹配的模式串即为 cur_node.word (如图例'ijabdf')
2. 当前节点的fail节点处 isWord = True, 则匹配的模式串为 cur_node.fail.word
(如图例 'abd',文字标红处有解释)
(当然fail节点也可能有fail.fail...需要while循环继续推一下.)
"""
ret = set()
cur_fail = cur_node.fail
if cur_node.isWord:
ret.add(cur_node.word)
#ret.append(cur_node.word)
while cur_fail is not None and cur_fail is not self.root:
if cur_fail.isWord:
#ret.append(cur_fail.word)
ret.add(cur_fail.word)
cur_fail = cur_fail.fail
return ret def search(self, s):
cur_node = self.root
# result = []
result = set()
cur_pos = 0 while cur_pos < len(seq):
word = seq[cur_pos]
#result.extend(self.curWords(cur_node)) while word in cur_node.children == False and cur_node != self.root:
# result.extend(self.curWords(cur_node))
result |= self.curWords(cur_node)
cur_node = cur_node.fail if word in cur_node.children:
#result.extend(self.curWords(cur_node))
result |= self.curWords(cur_node)
cur_node = cur_node.children[word] else:
cur_node = self.root result |= self.curWords(cur_node) cur_pos += 1 return list(result) ac = ACAutomation()
l = ['abd', 'abdk', 'abchijn', 'chnit', 'ijabdf', 'ijaij']
for e in l:
ac.add(e)
ac.link_fail()
seq = 'abchnijabdfk'
ret = ac.search(seq)
print(ret) """output
"""
# ['abd', 'ijabdf']

参考链接:

1. 多模字符串匹配算法之AC自动机—原理与实现:https://www.cnblogs.com/nullzx/p/7499397.html

2. 从头到尾彻底理解KMP:https://www.cnblogs.com/zhangtianq/p/5839909.html

[Alg] 文本匹配-多模匹配-AC自动机的更多相关文章

  1. 【JSOI2007】文本生成器 题解(AC自动机+动态规划)

    题目链接 题目大意:给定$n$个子串,要求构造一个长度为$m$的母串使得至少有一个子串是其子串.问方案数. ------------------------ 我们可以对要求进行转化:求出不合法的方案数 ...

  2. AC自动机算法详解 (转载)

    首先简要介绍一下AC自动机:Aho-Corasick automation,该算法在1975年产生于贝尔实验室,是著名的多模匹配算法之一.一个常见的例子就是给出n个单词,再给出一段包含m个字符的文章, ...

  3. AC自动机讲解+[HDU2222]:Keywords Search(AC自动机)

    首先,有这样一道题: 给你一个单词W和一个文章T,问W在T中出现了几次(原题见POJ3461). OK,so easy~ HASH or KMP 轻松解决. 那么还有一道例题: 给定n个长度不超过50 ...

  4. BZOJ1195[HNOI2006]最短母串——AC自动机+BFS+状态压缩

    题目描述 给定n个字符串(S1,S2,„,Sn),要求找到一个最短的字符串T,使得这n个字符串(S1,S2,„,Sn)都是T的子串. 输入 第一行是一个正整数n(n<=12),表示给定的字符串的 ...

  5. HDU 2222 Keywords Search(AC自动机)题解

    题意:给你几个keywords,再给你一段文章,问你keywords出现了几次. 思路:这里就要用到多模匹配算法AC自动机了,AC自动机需要KMP和字典树的知识,匹配时是在字典树上,失配我们就要用到类 ...

  6. 「kuangbin带你飞」专题十七 AC自动机

    layout: post title: 「kuangbin带你飞」专题十七 AC自动机 author: "luowentaoaa" catalog: true tags: - ku ...

  7. [NOI2011][bzoj2434] 阿狸的打字机 [AC自动机+dfs序+fail树+树状数组]

    题面 传送门 正文 最暴力的 最暴力的方法:把所有询问代表的字符串跑一遍kmp然后输出 稍微优化一下:把所有询问保存起来,把模板串相同的合并,求出next然后匹配 但是这两种方法本质没有区别,都是暴力 ...

  8. 【AC自动机】玄武密码

    [题目链接] https://loj.ac/problem/10058 [题意] 对于每一段文字,其前缀在母串上的最大匹配长度是多少呢 [参考别人的题解] https://www.luogu.org/ ...

  9. AC 自动机学习笔记

    虽然 NOIp 原地爆炸了,目前进入 AFO 状态,但感觉省选还是要冲一把,所以现在又来开始颓字符串辣 首先先复习一个很早很早就学过但忘记的算法--自动 AC AC自动机. AC 自动机能够在 \(\ ...

随机推荐

  1. 吴裕雄--天生自然 R语言开发学习:图形初阶

    # ----------------------------------------------------# # R in Action (2nd ed): Chapter 3 # # Gettin ...

  2. 介绍vue-cli脚手架config目录下index.js配置文件

    1.config/index.js var path = require('path') module.exports = { build: { // production 环境 env: requi ...

  3. 2018湖南省赛B题“2018”

    题面懒得敲了,反正看这篇博客的肯定知道题面. 比赛时想按约数的一些性质分情况讨论出公式然后在合并,结果单考虑矩阵里出现2018和1009(与2互质,1009出现次数等于2)出现的情况就写了一长串公式, ...

  4. keepalive笔记之一:基本安装

    在安装文件中有范例说明 /usr/share/doc/keepalived-1.2.13/samples/ Keepalived:它的诞生最初是为ipvs(一些服务,内核中的一些规则)提供高可用性的, ...

  5. js中escape的用法

    escape() 方法,它用于转义不能用明文正确发送的任何字符.比如,电话号码中的空格将被转换成字符 %20,从而能够在 URL 中传递这些字符.   var s="http://local ...

  6. 概念--Maven仓库

    转:Maven:mirror和repository 区别 Tip: 默认中央仓库的地址:https://repo.maven.apache.org/maven2 1.Maven仓库主要有2种 remo ...

  7. 漫说测试 | 研发虐我千百遍,我待bug如初恋

    的行业之一他们的运筹帷幄,他们的勾心斗角,只有自己知道.000,但绝对也是最枯燥的行业之一! IT可能是几个最高薪行业之一,但同时也绝对是最辛苦的行业之一!IT业是最需要创新能力的行业之一,但绝对也是 ...

  8. 让git push命令不再需要密码

    最近利用jekyll写博客,为的就是博客管理方便,但是在上传博客的时候使用git push命令每次都得输入github帐号和密码特别的不方便,于是就搜了一下. 在这篇文章里提到,GitHub获得远程库 ...

  9. 20190407-ORID

    2019-04-07 Objective 关于今天的课程,你记得什么? 给代码建立分支的操作 完成了什么? 完成了rails101前6节 Relective 今天的高峰是什么? 成功完成rails10 ...

  10. JavaScript 执行环境以及作用域链

    执行环境(execution context,为简单起见,有时也称为"环境")是 JavaScript 中最为重要的一个概念.执行环境定义了变量或函数有权访问的其他数据,决定了它们 ...