AC自动机详解

概述

  AC自动机全称Aho-Corasick automaton,该算法在1975年产生于贝尔实验室,是著名的多模匹配算法。

  考虑这样一个场景,给出L个模式字符串(加总长度为N),以及长度为M大文本,要求从大文本中提取每个模式字符串出现的位置。如果使用KMP算法,时间复杂度将达到O(LM+N),而使用AC自动机可以在O(N+M)时间复杂度内解决这一问题,当L很大时,AC自动机的优势非常明显。

建立AC自动机

  AC自动机实际上是前缀树,但是会引入一个与KMP类似的失败转移的概念。我们先为所有模式建立对应的前缀树,之后为每个前缀树结点添加一个指针fail,指向另外一个前缀树中的结点。每个前缀树中的结点实际上都代表了某个模式的一段前缀,我们之后将结点与其对应的前缀等同起来。令结点x的fail指针指向y(y不为x),其中y是x的后缀,且y是所有符合这类条件的结点中深度最大的(前缀长度最大的),我们称y是x的后缀结点,称x是y的伪父,显然伪父的伪父依旧还是伪父。可以很容易证明以x为起点沿着fail指针不断移动,可以遍历所有x的有效后缀,且访问到的结点深度递减。如果无法为结点的fail指针无法找到有效的结点,那么将fail指针指向前缀树的根结点root。

  AC自动机的难度在于要如何为每个结点建立fail指针。由于fail指针指向的结点深度必然小于fail指针的持有者,因此可以用DP的思路,我们先为深度较小的结点建立fail指针,再为深度较大的结点建立fail指针。这个过程可以通过广度优先搜索算法实现。要建立x的fail指针,考虑到x.fail.father必然是x.father的某个有效后缀,因此我们可以通过以x.father为起点,沿着fail指针移动以寻找x.fail.father,并从而找到x.fail。这个过程十分类似于KMP中建立跳转表的过程,这里对其具体操作不再赘述。

使用AC自动机

  如何使用AC自动机呢?我们维护一个轨迹结点trace,对于每个输入字符c,我们判断trace是否有c号孩子,如果有就将trace设置为其c号孩子,否则我们将trace设置trace.fail,并继续询问,直到trace成为root或者找到了c号孩子。重复上面过程直到读完文本。

  若最后trace成功设置为其c号孩子,则我们称访问了c号孩子。可以证明若输入文本T中T[a...b]与某个模式p相匹配,那么当我们读入T[b]时,p和p的所有伪父中有且只有一个结点被访问。*对于任意c

时间复杂度

  时间复杂度分为建立AC自动机的时间复杂度和匹配的时间复杂度。

  设所有模式的长度和为n,文本长度为m。建立前缀树的时间复杂度为O(n),而建立fail指针的时间复杂度分析类似于KMP算法中建立跳转表的时间复杂度。我们可以定义每个结点x的fail指针指向的y结点的深度为x的“子深”,记作x.cd。很容易发现x.cd<=x.father.cd+1,而我们每次从x.father出发沿着fail指针移动,x的子深也在不断递减但不会低于0,在为某个模式上的结点建立fail时,每次后移最多提供1个子深,因此在创建模式pi时我们最多沿着fail指针移动了|pi|次,故创建所有模式总共沿着fail指针最多移动O(n)次,到此说明了建立fail指针的时间复杂度为O(n)。

  对模式匹配,每当我们读入一个字符c时,trace或者向下移动(即有c号孩子)并结束或者沿着fail移动到某个自己的后缀上去。显然向下移动最多发生O(m)次,而沿着fail移动,就如同我所说的每次都必定会降低子深,而每次向下移动可以提供最多1子深,因此可以保证沿着fail移动的次数最多为O(m)次。故总的时间复杂度为O(m)。

  时间复杂度的总和为O(n+m),空间复杂度为O(Cn),其中C为使用的字符集的大小(用于建立前缀树)。

你可能感兴趣的:(AC自动机详解)