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;
	}
}


posted @ 2016-12-18 14:02  _Wilbert  阅读(162)  评论(0编辑  收藏  举报