本来这篇...我是不想写了的...以及比计划晚了三天...虽然是因为考试的原因....不过主要还是由于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;
}
}