字符串-AC自动机(详细图解)
AC自动机
AC自动机 (Aho-Corasick automaton)是KMP的升级版。即KMP是单模匹配算法,处理一个文本串中查找一个模式串的问题;而AC自动机能在一个文本串中同时查找多个不同的模式串 ,是多模匹配算法。
其实KMP也能做多模匹配,对每一个模式串做一次KMP,复杂度是 O ( k ( n + m ) ) O(k(n+m)) O ( k ( n + m ) ) ;AC自动机算法只需搜索一遍,搜索时匹配所有的模式串,复杂度是 O ( k m + n m ) O(km+nm) O ( k m + n m ) ,当 m < < k m<<k m < < k 时,AC自动机优势很大。
原理
建议先了解 KMP 和 字典树 。
那么如何同时匹配所有的模式串 P P P ?
这时结合KMP和字典树就可以开始秀了。如果把所有的 P P P 做成一个字典树,然后在匹配的时候查找这个 P P P 对应的 n e x t next n e xt ,即失败指针 F a i l Fail F a i l , 使当前字符失配时跳转到具有最长公共前后缀的字符继续匹配 (同KMP),这样就能实现同时搜索的快速匹配了。
步骤
- 构建字典树
- 构造fail失败指针
- 搜索待处理文本
Fail指针
同KMP的next一样,Fail指针是AC自动机的核心,是在树上指出失配后下一个跳转的位置,而不用全部回溯,大大减少时间。那么Fail是怎么跳转的?
以HDU-2222的样例为例说明,模式串P={“she”,“he”,“say”,“shr”,“her”},文本串S=“yasherhs”。
1.构建字典树
- 构造fail指针
2.1 用bfs实现,将root子节点入队(第二层),并将其fail指向root。
2.2 h出队,父节点h的fail指针所指节点是root;此时root没有对应为e的子节点,匹配失败,则e的fail指针指向root,表示没有匹配序列,然后入队e;同样的s出队,其子节点a同理。
2.3 此时循环到s的子节点h,父节点s的fail指针所指节点也是root,但与前面不同的是:root有值为h的子节点,匹配成功,此时fail应指向匹配节点。
2.4 以此类推,求出所有fail指针,右侧e的父节点h的fail指针所指节点是左侧h,而左侧h有值为e的子节点,匹配成功,即右侧e的fail指向左侧e(蓝线),如图。 - 搜索待处理文本
①首先根结点下无y和a,第1、2条线还是指向根结点;
②从she开始一直可以匹配,即线3、4、5,到节点e(绿底),更新答案;
③下一个字符是r,匹配失败,到fail指针所指节点(蓝线所指),即线6;
④此时匹配到了r(线7),发现模式串末尾标记,更新答案;
⑤下一个字符h,失配,回到fail所指(线8)
⑥然后继续匹配,成功(线9)
⑦继续下一个字符s,失配回溯(线10)
⑧继续匹配,成功(线11)。最后一个字符结束,退出
模板
void insert ( char * p ) { //构建字典树 int u = 0 ; int ls = strlen ( p ) ; for ( int i = 0 ; i < ls ; i ++ ) { int v = p [ i ] - 'a' ; if ( trie [ u ] [ v ] == 0 ) trie [ u ] [ v ] = ++ pos ; u = trie [ u ] [ v ] ; } cnt [ u ] ++ ; //当前节点单词数+1 } void getFail ( ) { //求fail queue < int > q ; for ( int i = 0 ; i < 26 ; i ++ ) { //入队root子节点(第二层) if ( trie [ 0 ] [ i ] ) { fail [ trie [ 0 ] [ i ] ] = 0 ; q . push ( trie [ 0 ] [ i ] ) ; } } while ( ! q . empty ( ) ) { int cur = q . front ( ) ; //当前父节点 q . pop ( ) ; for ( int i = 0 ; i < 26 ; i ++ ) { //26个字母 if ( trie [ cur ] [ i ] ) { //存在子节点,将其fail指向对应匹配节点(父节点fail所指节点的对应子节点) fail [ trie [ cur ] [ i ] ] = trie [ fail [ cur ] ] [ i ] ; q . push ( trie [ cur ] [ i ] ) ; } else //若不存在相关子节点,字典树中赋值为fail所指节点 trie [ cur ] [ i ] = trie [ fail [ cur ] ] [ i ] ; } } } int query ( char * s ) { //查询s中出现几个p int cur = 0 , ans = 0 , ls = strlen ( s ) ; for ( int i = 0 ; i < ls ; i ++ ) { cur = trie [ cur ] [ s [ i ] - 'a' ]