前置技能
简介
看dalao们AC自动机的Blog,大多数奆奆都会感性地说: AC_automation = KMP+TRIE
<!--more--> 然而在作者重蹈覆辙辗转反侧n次后才明白,这东西说了等于没说。
- AC自动机是一种有限状态自动机(说了等于没说),它常被用于多模式串的字符串匹配。
- 在学完AC自动机,笔者也总结出一句说了等于没说的话: AC自动机是以TRIE的结构为基础,结合KMP的思想建立的。
建立AC自动机
建立一个AC自动机通常需要两个步骤:
- 基础的TRIE结构:将所有的模式串构成一棵Trie。
- KMP的思想:对Trie树上所有的结点构造失配指针。
然后就可以利用它进行多模式匹配了。
TRIE构建
- 和trie的insert操作一模一样(强调!一模一样!)
- 因为我们只利用TRIE的结构,所以只需将模式串存入即可。
构造失配( failfailfail )指针
在讲构造以前,先来对比一下这里的 failfailfail 指针与KMP中的next指针:
- 共同点-两者同样是在失配的时候用于跳转的指针。
- 不同点-KMP要求的是最长相同真前后缀,而AC自动机只需要相同后缀即可。
- 因为KMP只对一个模式串做匹配,而AC自动机要对多个模式串做匹配。
- 有可能 failfailfail 指针指向的结点对应着另一个模式串,两者前缀不同。
- 也就是说,AC自动机在对匹配串做逐位匹配时,同一位上可能匹配多个模式串。
- 因此 failfailfail 指针会在字典树上的结点来回穿梭,而不像KMP在线性结构上跳转。
下面介绍构建 failfailfail 指针的基础思想:(强调!基础思想!基础!)
- 构建 failfailfail 指针,可以参考KMP中构造next数组的思想。
- 我们利用部分已经求出 failfailfail 指针的结点推导出当前结点的 failfailfail 指针。具体我们用BFS实现:
- 考虑字典树中当前的节点u,u的父节点是p,p通过字符c的边指向u。
- 假设深度小于u的所有节点的 failfailfail 指针都已求得。那么p的 failfailfail 指针显然也已求得。
- 我们跳转到p的 failfailfail 指针指向的结点 fail[p]fail[p]fail[p] ;
- 如果结点 fail[p]fail[p]fail[p] 通过字母 ccc 连接到的子结点 www 存在:
- 则让u的fail指针指向这个结点 www ( fail[u]=wfail[u]=wfail[u]=w )。
- 相当于在 ppp 和 fail[p]fail[p]fail[p] 后面加一个字符 ccc ,就构成了 fail[u]fail[u]fail[u] 。
- 如果 fail[p]fail[p]fail[p] 通过字母 ccc 连接到的子结点 www 不存在:
- 那么我们继续找到 fail[fail[p]]fail[fail[p]]fail[fail[p]] 指针指向的结点,重复上述判断过程,一直跳 failfailfail 指针直到根节点。
- 如果真的没有,就令 fail[u]=fail[u]=fail[u]= 根节点。
- 如果结点 fail[p]fail[p]fail[p] 通过字母 ccc 连接到的子结点 www 存在:
- 如此即完成了 failfailfail 指针的构建。
下面放一张GIF帮助大家理解: 对字典树{i,he,his,she,hers}构建 failfailfail 指针:
-
黄色结点表示当前的结点u,绿色结点表示已经BFS遍历完毕的结点,红/橙色的边表示 failfailfail 指针。
-
2号节点的 failfailfail 指针画错了, fail[2]=0fail[2]=0fail[2]=0 .
-
我们重点分析结点6的 failfailfail 指针构建:
-
找到6的父节点5,5的 failfailfail 指针指向10,然而10结点没有字母's'连出的边;
-
所以跳到10的 failfailfail 指针指向的结点0,发现0结点有字母's'连出的边,指向7结点;
-
所以 fail[6]=7fail[6]=7fail[6]=7 .
另外,在构建 failfailfail 指针的同时,我们也对TRIE中模式串的结尾构建 failfailfail 指针。这样在匹配到结尾后能自动跳转到下一个匹配项。具体见代码实现。
从代码深入剖析
框架
const int N=1000;
struct AC_automaton{
int tr[N][26],cnt;//TRIE
int e[N];//标记这个结点是不是字符串结尾
int fail[N];//fail指针
<span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">insert</span><span class="hljs-params">(<span class="hljs-keyword">char</span> * s)</span></span>{}<span class="hljs-comment">//插入模式串</span>
<span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">build</span><span class="hljs-params">()</span></span>{}<span class="hljs-comment">//构建fail指针</span>
<span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">query</span><span class="hljs-params">(<span class="hljs-keyword">char</span> *t)</span></span>{}<span class="hljs-comment">//匹配函数</span>