AC自动机
本来这篇...我是不想写了的...以及比计划晚了三天...虽然是因为考试的原因....不过主要还是由于AC自动机这个算法我也不过是上周日的时候才学会怎么写。原理性东西有点了解而已。
所以既然还是决定写了,那就写吧。
AC自动机算法(Aho-Corasick算法)是由Alfred V. Aho和Margaret J.Corasick 发明的字符串搜索算法,在均摊情况下,具有近似于线性的时间复杂度。
这个算法主要利用的就是一个Trie树,再加上一个失配指针,在"Trie树上跑KMP算法"。这个算法通常用于在一段文章中查找字典中的字符串出现的次数。
首先,构造Trie树这个想必都是会的。然后就是构造失配指针。
我所理解的失配指针,就是在当前匹配到失败的时候,跳转到另外一个与自身相同且具有相同前缀的字符上,如果没有相同的字符,就转到root上。不过这里所说的前缀,与一个字符串的前缀不同,但也类似吧。
假设有一个如下图的字典:
可以看出来,其字典里的词尾say,she,shr,her,hr,构造的Trie如上图。
那么对于she中的he失配时,就会跳到her上的e字符,在这个过程中,her前缀为he,she的前缀也是he,不是通常意义上的前缀。(嘛...这只是我这一个星期的理解....可能不准确...以后理解好了回来在进一步阐述吧...
其实这里的失配指针与KMP算法的失配指针非常的类似,所以才会经常说AC自动机算法就是在Trie树上跑KMP算法。
所以最后构造的失配指针如下图:
然后就是进行匹配了。
对于给定字符串yasherhs
首先对y匹配,而root的孩子中并没有y,于是对a匹配,同样也没有这个孩子,于是匹配到了s,发现s是root的孩子,于是字典树走到s;匹配h,刚好s的孩子中有一个为h,于是继续走到h;匹配e,刚好h有一个孩子是e,这时候经过判断得知,这已经是字典中she单词的最后一个单词,于是匹配到了这个单词。继续匹配,当这个单词匹配到了一个单词之后,还是转到了失配指针指向的地方,因为即使匹配了,对于下一个字符来说,也是失配了。所以走到第一层的h的孩子e上,与r匹配,失配,走到root上(图上蓝色线忘记画这条线了不用在意细节....),然后匹配h,发现root有这个孩子........最终匹配到了一个she
下面给出C++的AC自动机代码:
#include <bits/stdc++.h> using namespace std; const int maxn = 26; const int maxm = 1e6 + 5; typedef struct node{ node *fail; //失配指针 node *child[maxn]; //儿子节点 int point; //标识这是第几个模式串的终止节点 node() { fail = NULL; for( int i = 0; i < maxn; ++i ) child[i] = NULL; point = -1; } }Trie; Trie *root; char str[maxm]; void Insert( char *s, int num ) { Trie *p = root; for( char *c = s; *c != '\0'; ++c ) { int t = (*c) - 'a'; if( p -> child[t] == NULL ) { p -> child[t] = new Trie; } p = p -> child[t]; if( (*(c + 1)) == '\0' ) p -> point = num; } } void buildFailPointer() { queue<Trie* > q; while( !q.empty() ) q.pop(); q.push(root); while( !q.empty() ) { Trie *now = q.front(); q.pop(); for( int i = 0; i < maxn; ++i ) { if( now -> child[i] != NULL) { if ( now == root ) now -> child[i] -> fail = root; else { Trie *p = now -> fail; while( p != NULL ) { if( p -> child[i] != NULL ) { now -> child[i] -> fail = p -> child[i]; break; } p = p -> fail; } if ( p == NULL ) now -> child[i] -> fail = root; } q.push( now -> child[i] ); } } } } int AC_auto() { int ret = 0; Trie *p = root; int len = strlen(str); for( int i = 0; i < len; ++i ) { int inx = str[i] - 'a'; while( p -> child[inx] == NULL && p != root ) p = p -> fail; if( p -> child[inx] == NULL ) continue; p = p -> child[inx]; Trie *t = p; while( t != root ) { if( t -> point != -1 ) ++ret; t = t -> fail; } } return ret; } int main() { root = new Trie; Insert( "she", 1 ); Insert( "he", 2 ); Insert( "say", 3 ); Insert( "shr", 4 ); Insert( "her", 5 ); buildFailPointer(); str[0] = 'y'; str[1] = 'a'; str[2] = 's'; str[3] = 'h'; str[4] = 'e'; str[5] = 'r'; str[6] = 'h'; str[7] = 's'; cout << AC_auto() << endl; return 0; }
然而在写hihocoder第四周的时候,发现仅仅是AC自动机的话依旧是会超时的。所以发现其实有一个更加优化的算法,叫做Trie图,其实也就是AC自动机的优化版。不过这个算法。。。萌新只会喊666然后复制dalao代码,所以以后再更新这里吧。
给出hihocoder-1036-Trie图的Java代码:
import java.util.Scanner; import java.util.ArrayList; import java.io.BufferedInputStream; public class Main{ public static void main( String[] args ) { @SuppressWarnings("resource") Scanner in = new Scanner( new BufferedInputStream( System.in ) ); int n = Integer.parseInt( in.nextLine() ); TrieGraph tg = new TrieGraph(); while(n-- > 0) { String str = in.nextLine(); tg.add(str); } tg.build(); String str = in.nextLine(); if( tg.ac_auto( str ) ) { System.out.println("YES"); } else { System.out.println( "NO" ); } } } class Node{ public char val = '\0'; public Node par = null; public Node fail = null; public boolean isEnd = false; private static int maxn = 26; public Node[] ch = new Node[maxn]; Node( ) { this.val = '\0'; this.par = this; } Node( char c, Node p ) { this.val = c; this.par = p; } public Node getNext( char c ) { Node next = this.ch[ c - 'a' ]; if( next == null ) next = this.ch[ c - 'a' ] = new Node( c, this ); return next; } public void setRootFail( Node root ) { this.fail = root; } public void setFail( ) { this.fail = par.fail.ch[ this.val - 'a' ]; } public void setNext( int index ) { this.ch[ index ] = fail.ch[ index ]; } } class TrieGraph{ private Node root = new Node( '$', null ); public void add( String str ) { int len = str.length(); Node p = root; for( int i = 0; i < len; ++i ) { p = p.getNext( str.charAt(i) ); } p.isEnd = true; } public void build() { ArrayList<Node> q = new ArrayList<Node>(); root.setRootFail(root); for ( int i = 0; i < 26; ++i ) { if( root.ch[i] == null ) { root.ch[i] = root; } else { q.add( root.ch[i] ); } } while( q.size() > 0 ) { Node p = q.get(0); if( p.par == root ) { p.setRootFail(root); } else { p.setFail(); } for ( int i = 0; i < 26; ++i ) { if( p.ch[i] == null ) { p.setNext( i ); } else { q.add( p.ch[i] ); } } q.remove(0); } } public boolean ac_auto( String str ) { Node p = root; int len = str.length(); for( int i = 0; i < len; ++i ) { p = p.getNext( str.charAt(i) ); if( p.isEnd ) return true; } return false; } }