[题解][笔记]AC自动机

[题解][笔记]AC自动机

算法概述:

AC自动机的用途主要是在一个文本串中查询多个短字符串,它的主要构成就是一颗\(trie\)树再加上一些\(kmp\)的思想.

模拟算法过程:

​ 首先给定\(4\)个模式串:\(ash\),\(shex\),\(bcd\),\(sha\).并建立一颗\(trie\)树.

接着,我们之前说过\(ac\)自动机就是\(trie\)树加\(kmp\)的思想,那么\(kmp\)\(border\)用来提升效率,\(ac\)自动机也有相类似的结构:\(fail\)指针.它的用处就是如果当前点失配了就直接把指针移动到\(fail\)指针所指的节点,就不需要回溯了.(\(fail\)指针指向的要求:当前模式串后缀和\(fail\)指针指向的模式串的前缀是相同的)例如:当前找的是\(abce\),但我们发现要匹配的第四位发现并不是要找\(e\),y于是跳\(fail\)指针,看\(bcd\)符不符合要求.(注意:上文括号中的当前模式串是指从根节点到这个节点上所有字符组成的字符串)

​ 构造\(fail\)指针通常通过广搜的办法实现.对于根节点的儿子节点,它们的\(fail\)指针直接指向根节点,因为它们没有后缀或前缀.对于其他节点,如果有一样的节点就直接相连,如果,没有一样的节点就连到根节点.


建好的\(AC\)自动机,引用自bestsort

看代码吧,里面有注释

#include <bits/stdc++.h> 
using namespace std;
char str[1000010];
struct node{
	int fail;//失配指针
	int cnt;//单词出现的次数
	int next[62];//儿子节点
}trie[1000010];
int k = 0,ans = 0;
queue< int > q;
void build_trie(int id,char s[]){
	int len = strlen(s);//字符串的长度,也是这个字符串在trie树中的深度(可以看上图理解下)
	int j = 0;
	for(int i = 0;i < len;i++){//遍历输入的字符串
	    j = s[i] - 'a';
	    if(trie[id].next[j]==0){//如果当前节点的儿子中还没有这个字符就新建节点
	        trie[id].next[j]= ++k;//节点个数+1
	    }
	    id = trie[id].next[j];//因为是建trie树,所以建完的字符串是一个从上到下的链,所以建完当前节点就建它的儿子节点
	}
	trie[id].cnt++;//单词数量+1
}
void build_fail(int id){
	while(!q.empty())q.pop();
	for(int i = 0;i < 26;i++){//遍历根节点的所有儿子节点
	    int j = trie[id].next[i];
	    if(j != 0){//如果存在这个儿子就入队
	        q.push(j);
	        trie[j].fail = id;//根节点的儿子的fail指针就是根节点
	    }
	}
	while(!q.empty()){//开始广搜
	    int now = q.front();
		q.pop();
	    for(int i = 0;i < 26;i++){//遍历当前节点的儿子
	        int j = trie[now].next[i];
	        if(j == 0){//如果没有这个儿子
	            trie[now].next[i] = trie[trie[now].fail].next[i];//它的儿子是它父亲fail指针指向的节点的对应的儿子
                //因为trie数是1~26代表a~z,所以next数组里的第i个字母一定对应的是字母表中的第i个字符
                //所以如果当前节点没有"h"这个儿子,那么在它指向的节点一定也是"h"
	            continue;
	        }
	        trie[j].fail = trie[trie[now].fail].next[i];//当前节点的儿子的fail指针指向它父亲fail指针指向的节点的对应的儿子
	        q.push(j);
	    }
	}
}
void sovle(int id,char s[]){
	int len = strlen(s),j = 0;
	for(int i=0;i<len;i++){
	    int j = trie[id].next[s[i] - 'a'];
	    while(j && trie[j].cnt != -1){//如果存在这个节点并且这个节点有构成单词
	        ans += trie[j].cnt;//答案累计合法单词的数量
	        trie[j].cnt = -1;//不能重复累加
	        j = trie[j].fail;//直接指向它的失配指针
	    }
	    id = trie[id].next[s[i] - 'a'];//往下找
	}
}
int main(){
	int n;
	scanf("%d",&n);
	for(int i = 1;i <= n;i++){
	    scanf("%s",str);
	    build_trie(0,str);
	}
	build_fail(0); 
	scanf("%s",str);
	sovle(0,str); 
	printf("%d\n",ans); 
	return 0;
} 

注意:

这个题在查询的时候,如果当前单词已经被访问过(也就是trie[j].cnt==-1)就跳出循环,因为跳\(fail\)其实是跳当前点到根组成的字符串的后缀,所以既然当前点已经被访问过了,它的后缀也肯定被访问过了,而另一道题,ac自动机加强版就不同,加强版中是统计出现的次数而不是是否出现过,就与这个题不同,在文本串不同位置出现的某个模式串是要被重复计算的,所以就不能因为已经访问过就直接跳出循环,可能是在文本串的前一段中出现过,在后一段中又出现了,所以都要计入答案,要注意!!!

posted @ 2020-10-26 10:32  czyczy  阅读(74)  评论(0编辑  收藏  举报