AC自动机

引入

什么是自动机?

OI中所说的“自动机”一般都指“确定有限状态自动机”。自动机是OI、计算机科学中被广泛使用的一个数学模型,其思想在许多字符串算法中都有涉及,因此推荐在学习一些字符串算法(KMP、AC 自动机、SAM)前先完成自动机的学习。学习自动机有助于理解上述算法。

好的 其实上面并没有什么软用

AC自动机(Aho-Corasick automaton) 可以大概理解为是在Trie数上跑KMP 用于解决单文本多模式串匹配问题

KMP与Trie树

这边就不再赘述 若不会请读者自行学习 KMP学习 Tire树学习

KMP模板

基本的KMP 这边直接放代码 对着读一下应该没太大问题

#include<string.h>
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<vector>
#include<map>
#include<set>

using namespace std;
typedef long long LL ;
typedef pair<LL,LL> pii ;
#define fre(x) freopen(#x".in","r",stdin) ; freopen(#x".out","w",stdout) ;
#define re register
#define temp template<typename T>

temp
inline T read (T x = 0 , T f = 0){
	re char ch = getchar() ;
	for (;!isdigit(ch);ch = getchar() ) if (ch == '-') f = 1 ;
	for (;isdigit(ch);ch = getchar()) x = (x<<1) + (x<<3) + (ch^48) ;
	return f? ~x+1 : x ;
}
const LL N = 1e6+5 ;
string s1 , s2 ; 
LL len1 , len2 ;
LL Next[N] ;
vector<LL> ans ;
inline void KMP(){
	LL k = 0 ;
	for(LL i = 1 ; i <= len1 ; ++i ){
		while(k && s1[i] != s2[k+1] ) k = Next[k] ;
		if(s2[k+1] == s1[i] ) ++k ;
		if(k == len2 ){
			ans.push_back(i - k);
			k = Next[k] ;
		}
	}
	return ;
}
inline void Getnext(){
	Next[0] = 0 ;
	LL k = 0 ;
	for(LL i = 2 ; i <= len2 ; ++i ){
		while(k && s2[k+1] != s2[i] ) k = Next[k] ;
		if(s2[k+1] == s2[i]) ++k ;
		Next[i] = k ; 
	}
	return ;
}
signed main(){
	cin >> s1 >> s2 ;
	s1 = ' ' + s1 ;
	s2 = ' ' + s2 ; 
	len1 = s1.length() - 1 ;
	len2 = s2.length() - 1 ;
	Getnext() ;
	KMP() ;
	if(ans.size())
		for(LL i:ans){
			printf("%lld\n",i+1) ;
		}
	for(LL i = 1 ; i <= len2 ; ++i){
		printf("%lld ",Next[i]<0?0:Next[i]) ;		
	}
	return 0; 
}

昂……Trie树的话 Acwing前缀统计[Trie树模板]

代码也放在这里了

#include <bits/stdc++.h>

using namespace std ;

int read(int x = 0,bool f = false,char ch = getchar()) {
    for (;!isdigit(ch);ch = getchar()) f |= ch == '-' ;
    for (;isdigit(ch);ch = getchar()) x = (x << 1) + (x << 3) + (ch ^ 48) ; return f ? ~ x + 1 : x ;
}

class Trie {
    private :
        int Tp , value_type ;
        Trie *son[26] ;
    public :
        Trie(int x):Tp(x){
            for (int i = 0 ; i < 26 ; ++i ) son[i] = NULL ;
        }
        void insert(Trie* &Root,char str[]) {
            int len = strlen(str) ;
            Trie* p = Root ;
            for (int i = 0 ; i < len ; ++i ) {
                int alpha = str[i] - 'a' ;
                if (p -> son[alpha] == NULL) {
                    p -> son[alpha] = new Trie(0) ;
                    p -> son[alpha] -> value_type = alpha ;
                }
                p = p -> son[alpha] ;
            }
            ++ p -> Tp ;
        }
        int search(Trie* &Root,char str[]) {
            int len = strlen(str) , ans = 0 ;
            Trie* p = Root ;
            for (int i = 0 ; i < len ; ++i ) {
                int alpha = str[i] - 'a' ;
                if (p -> son[alpha] == NULL) return ans ;
                p = p -> son[alpha] ;
                ans += p -> Tp ;
            } return ans ;
        }
} ;

const int N = 1e6 + 5 ;
int n , m ;
char str[N] ;

signed main() {
    Trie* Answer = new Trie(0) ;
    n = read() ; m = read() ;
    for (int i = 1 ; i <= n ; ++i )
        scanf("%s",str) , Answer -> insert(Answer,str) ;
    for (int i = 1 ; i <= m ; ++i )
        scanf("%s",str) , printf("%d\n",Answer -> search(Answer,str)) ;
    return 0 ;
}

正题AC自动机

Build构建

  1. 建立一个Trie树 并把模式串插入

  2. 为每一个点建立一个失配指针Fail(PS:失配 即在此处匹配失效 下面会具体讲到)

Fail失配指针

定义

一个状态\(u\)指向另一个状态\(v\) 其中\(v \in Trie\) 求v为u的最长后缀(即若干个后缀状态取一个最长的)

就大概类比一下KMP的Next指针

但也稍有不同

  • 共同点 两者都是失配后的跳转指针

  • 不同点 Next求最长的Border 而Fail指向的是所有状态的前缀指向当前状态的最长后缀

没看懂 那看看我从Oi-wiki上扒下来的图

Graph from OI-WIKI

对于现在有一个u状态 它的父亲节点为fa fa通过了alpha字符边指向了u 也就是\(Trie_{fa,alpha} = u\) 并且假设深度小于u的节点的Fail指针都已经求得

  1. \(Trie[fail[fa],alpha]\)存在 那么我们直接让\(fail[u]\)指向这玩意

  2. \(Trie[fail[fa],alpha]\)不存在 那咋滴子办吗 这不是显然吗 直接找\(Trie[fail[fail[fa]],alpha]\)存不存在 再一直跳上去 跳到根节点 不过到哪里都没有 那就真没办法了 指向根节点吧

那么如何做到深度小的都先求出来呢 —— BFS! 因为Trie是一颗树 阿Sir

下面是代码实现

void spawn_fail(Aho_Corasick_automaton* &Rt) {
    queue<Aho_Corasick_automaton*> Q ;
    Aho_Corasick_automaton* u ;
    for(int i = 0; i < 30; i++) {
        if(Rt -> son[i] != NULL)
            Rt -> son[i] -> nxt = Rt , Q.push(Rt -> son[i]) ;
        else
            Rt -> son[i] = Rt ;
    }
    while(Q.size()) {
        u = Q.front() ; Q.pop() ;
        for (int i = 0 ; i < 30 ; ++i )
            if (u -> son[i] == NULL) {
                u -> son[i] = u -> nxt -> son[i] ;
            } else {
                Q.push(u -> son[i]) ;
                u -> son[i] -> nxt = u -> nxt -> son[i] ;
            }
    }
}

别问变量为什么那么长 因为有 自动补全 真心建议用Visual Studio或者Visual Studio Code欸嘿

询问Query

这个跟KMP真的差不多
把文本串在Trie树上扫一遍 再扫的途中失配或者匹配完了通过\(Fail\)跳转一下就行了

在匹配字符串的过程中,我们会舍弃部分前缀达到最低限度的匹配。\(Fail\)指针则指向了更多的匹配状态。

我的记录方法有亿些奇怪 由于用指针写 访问下标不要太麻烦
于是乎 直接把这个节点的权值(即是模式串尾的个数)赋值为\(-1\)

因为不可能有\(-1\)个串经过这里吧…… 注意如果这样写 根节点也需要赋成\(-1\)

贴上我的代码

void Find(Aho_Corasick_automaton* &Rt,char str[]) {
    ans = 0 ; Aho_Corasick_automaton* o = Rt ;
    len = strlen(str) ;
    for (int i = 0 ; i < len ; ++i ) {
        alpha = str[i] - 'a' ;
        Aho_Corasick_automaton* p = o -> son[alpha] ;
        while (p -> Tp != -1) {
            ans += p -> Tp ;
            p -> Tp = -1 ;
            p = p -> nxt ;
        } o = o -> son[alpha] ;
    }
}

例题

模板题

综上所述 直接贴代码了

#include <bits/stdc++.h>

using namespace std ;

int read(int x = 0,bool f = false,char ch = getchar()) {
    for (;!isdigit(ch);ch = getchar()) f |= ch == '-' ;
    for (;isdigit(ch);ch = getchar()) x = (x << 1) + (x << 3) + (ch ^ 48) ;
    return f ? ~ x + 1 : x ;
}

const int N = 1e5 + 5 , L = 1e6 + 5 ;

int tmp , cnt = 1 , ans , n , len , alpha ;

class Aho_Corasick_automaton {
    private :
        int Tp , idx , value_type ;
        Aho_Corasick_automaton *son[30] , *nxt ;
    public :
        Aho_Corasick_automaton (int x):Tp(x) {
            value_type = 0 , idx = ++ cnt , nxt = NULL ;
            for (int i = 0 ; i < 30 ; ++i )
                son[i] = NULL ;
        }
        void Insert (Aho_Corasick_automaton* &Rt,char str[]) {
            Aho_Corasick_automaton* o = Rt ;
            len = strlen(str) ;
            for (int i = 0 ; i < len ; ++i ) {
                alpha = str[i] - 'a' ;
                if (o -> son[alpha] == NULL) o -> son[alpha] = new Aho_Corasick_automaton(0) , o -> son[alpha] -> value_type = alpha ;
                o = o -> son[alpha] ;
            }
            ++ o -> Tp ;
        }
        void spawn_fail(Aho_Corasick_automaton* &Rt) {
            queue<Aho_Corasick_automaton*> Q ;
            Aho_Corasick_automaton* u ;
            for(int i = 0; i < 30; i++) {
                if(Rt -> son[i] != NULL)
                    Rt -> son[i] -> nxt = Rt , Q.push(Rt -> son[i]) ;
                else
                    Rt -> son[i] = Rt ;
            }
            while(Q.size()) {
                u = Q.front() ; Q.pop() ;
                for (int i = 0 ; i < 30 ; ++i )
                    if (u -> son[i] == NULL) {
                        u -> son[i] = u -> nxt -> son[i] ;
                    } else {
                        Q.push(u -> son[i]) ;
                        u -> son[i] -> nxt = u -> nxt -> son[i] ;
                    }
            }
        }
        void Find(Aho_Corasick_automaton* &Rt,char str[]) {
            ans = 0 ; Aho_Corasick_automaton* o = Rt ;
            len = strlen(str) ;
            for (int i = 0 ; i < len ; ++i ) {
                alpha = str[i] - 'a' ;
                Aho_Corasick_automaton* p = o -> son[alpha] ;
                while (p -> Tp != -1) {
                    ans += p -> Tp ;
                    p -> Tp = -1 ;
                    p = p -> nxt ;
                } o = o -> son[alpha] ;
            }
        }
} ;

int T ;
char str[L] , substr[L] ;

signed main() {
    n = read() ; Aho_Corasick_automaton* Answer = new Aho_Corasick_automaton(-1) ;
    for (int i = 1 ; i <= n ; ++i ) scanf("%s",str) , Answer -> Insert(Answer,str) ;
    scanf("%s",substr) ; Answer -> spawn_fail(Answer) ;
    Answer -> Find(Answer,substr) ;
    printf("%d\n",ans) ;
}

LOJ上还有一系列的AC自动机的好题

而且还有一定的优化或者改善 能够帮助大家理解AC自动机

posted @ 2021-08-21 07:52  xxcxu  阅读(117)  评论(1编辑  收藏  举报