AC自动机学习笔记

前置技能

必要前置技能:Trie
非必要前置技能:KMP

引入问题

n n n个模式串,一个文本串,求有多少个模式串在文本串中出现过。
暴力有两种方式:第一种是用所有模式串用 KMP 与文本串匹配,第二种是对所有模式串建一棵Trie,然后对于每个文本串的前缀在Trie中统计结果。AC自动机对第二种方法进行了优化。
洛咕P3808 【模板】AC自动机(简单版)

AC自动机构造方法和搜索方法简述

类似于Trie,AC自动机中的点表示状态,边表示当下一个字符是这条边对应的字符时转移到什么状态。
建好AC自动机后,根据文本串在自动机中搜索并统计信息,得出答案。

AC自动机的构造方法:先按 n n n个模式串建一棵Trie。如果只建Trie,那么就是暴力了。考虑匹配时的过程。失配时我们不想放弃前面匹配过的结果,所以考虑怎样保留当前匹配的结果(这里与kmp有点像)。
令失配指针 f a i l fail fail表示最长的与当前状态的后缀相同,且不为自身的字符串对应的状态。当失配时,就往 f a i l fail fail上跳,这样就保证这个状态在文本串中出现过,且是不失配的最长的状态。
x x x f a i l fail fail链表示 x x x不断往 f a i l [ x ] fail[x] fail[x]跳的过程中出现的所有节点组成的链。

举个例子,假设 n = 4 n=4 n=4,字符串都只有AB两种字符, 4 4 4个文本串分别为"AA",“AB”,“B”,“BA”。
先建出Trie:
Trie
“A"这个状态显然不会失配(因为无论下一个字符是A还是B都可以继续匹配)。
“AA"这个状态,最长的与它后缀相同且不为它本身的状态为"A”。所以它的失配指针指向"A"这个状态。
“AB"这个状态,最长的与它后缀相同且不为它本身的状态为"B”。所以它的失配指针指向"B"这个状态。
“B"这个状态,最长的与它后缀相同且不为它本身的状态为””(空字符串)。所以它的失配指针指向根节点。
“BA"这个状态,最长的与它后缀相同且不为它本身的状态为"A”。所以它的失配指针指向"A"这个状态。
用红色箭头表示 f a i l fail fail指针的指向,可以得到这样的AC自动机:
在这里插入图片描述
是不是记录下模式串结尾的位置,然后根据Trie树和失配指针走就珂以了呢?
模拟一下,比如"BAA"这个字符串:
第一个字符B,可以匹配,从根节点走到"B"这个状态。发现"B"这个节点是一个模式串的结尾,所以答案+1。
第二个字符A,可以匹配,从"B"状态走到"BA"状态。发现"BA"这个节点是一个模式串的结尾,所以答案+1。
第三个字符A,不能匹配,从"BA"状态跳到失配指针处,即"A"状态。显然这个跳到的状态"A"在文本串中出现过(第一个A)。第三个字符是A,所以从"A"状态走到"AA"状态,发现"AA"这个节点是一个模式串的结尾,答案+1。
最后我们得到的答案是3,而"BAA"这个字符串中包含了"B"、“BA"和"AA"这3个字符串。
完美!
……
……
……真的完美吗?
不妨考虑一下文本串为"AB"时会怎么走。不难发现它只会统计到一个模式串,就是"AB”。但显然文本串"AB"包含了模式串"B",而"B"并没有统计到。
发现若有一个模式串被这个状态包含,这种方法是统计不到的。考虑失配指针表示的是最长的与当前状态的后缀相同,且不为自身的状态。
所以每次搜索到一个状态时,沿着它的fail链不断跳上去(就是不断地跳到这个状态的失配指针处),统计fail链上有多少模式串即珂。

AC自动机实现

建AC自动机时,先建普通的Trie。算失配指针 f a i l fail fail时,可以用bfs的方法,自根节点向下遍历,用父亲节点的 f a i l fail fail推出孩子的 f a i l fail fail
具体见代码:

int n,tot=1;
struct node {
	int ch[27];
} w[Size];
int word[Size];		//word[i]表示i号节点上有多少个模式串的结尾 
void insert(char *str) {
	int len=strlen(str),rt=1;
	for(re i=0; i<len; i++) {
		int p=str[i]-'a';
		rt=w[rt].ch[p]?w[rt].ch[p]:w[rt].ch[p]=++tot;
	}
	word[rt]++;
}
int fail[Size],Queue[Size];
void bfs() {		//通过bfs建AC自动机 
	for(re i=0; i<26; i++)	w[0].ch[i]=1;
	int hd=0,tl=0;
	Queue[++tl]=1;
	while(hd<tl) {
		int x=Queue[++hd];
		for(re i=0; i<26; i++) {
			//这里对求fail的部分进行了优化 
			//本来求fail的方法是沿着fail链往上跳,找到第一个能匹配i+'a'字符的状态 
			//这里如果一个状态无法匹配某个字符 
			//我们把它的这个字符对应的孩子改为fail链上最长的能匹配这个字符的状态 
			//这样就能保证跳到的一定是能匹配这个字符的状态 
			if(w[x].ch[i]) {
				fail[w[x].ch[i]]=w[fail[x]].ch[i];
				Queue[++tl]=w[x].ch[i];
			} else {
				w[x].ch[i]=w[fail[x]].ch[i];
			}
		}
	}
}

查询时每次需要沿着 f a i l fail fail链往上跳。为了防止重复统计,用一个vis数组保存某个节点以及它的 f a i l fail fail链是否被统计过了。具体见代码。

bool vis[Size];
int query(char *str) {
	int len=strlen(str),rt=1,ans=0;
	for(re i=0; i<len; i++) {
		rt=w[rt].ch[str[i]-'a'];
		for(re j=rt; j && !vis[j]; j=fail[j]) {
			//如果前面统计过j(即vis[j]==true),那么fail链上剩下的就不需统计了 
			vis[j]=true;
			ans+=word[j];
		}
	}
	return ans;
}

然后洛咕P3808就珂以A了qwq。

例题

洛咕P3966 [TJOI2013]单词
题意:一篇由 n n n个小写字母组成的单词组成的文章,问对于每个单词,它在文章中多少个地方出现了。

发现若 y y y x x x f a i l fail fail链上的节点,则 y y y的状态一定是 x x x的状态的后缀。
题目求的是某个单词在多少个地方出现,即它是多少个状态的后缀。也就是说,需要求的是某个单词在多少个状态的 f a i l fail fail链上。
因为一个节点的 f a i l fail fail链上的节点的bfs序都比它小,所以建出AC自动机,然后按照bfs序来把自身的值叠加到 f a i l fail fail处即可。

#include<stdio.h>
#include<cstring>
#include<algorithm>
#include<math.h>
#define re register int
#define mod 1000000007
using namespace std;
typedef pair<int,int> pii;
typedef long long ll;
int read() {
	re x=0,f=1;
	char ch=getchar();
	while(ch<'0' || ch>'9') {
		if(ch=='-')	f=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9') {
		x=10*x+ch-'0';
		ch=getchar();
	}
	return x*f;
}
inline void write(const int x) {
	if(x>9)	write(x/10);
	putchar(x%10+'0');
}
const int Size=1000005;
const int INF=0x3f3f3f3f;
int n,tot=1,id[Size];
char now[Size];
struct node {
	int ch[27];
	int cnt;
	int fail;
} w[Size];
void insert(char *str,int num) {
	int rt=1,len=strlen(str);
	w[rt].cnt++;
	for(re i=0; i<len; i++) {
		int p=str[i]-'a';
		rt=w[rt].ch[p]?w[rt].ch[p]:w[rt].ch[p]=++tot;
		w[rt].cnt++;
	}
	id[num]=rt;
}
int Queue[Size],sum[Size];
void getfail() {
	for(re i=0; i<26; i++)	w[0].ch[i]=1;
	int hd=0,tl=0;
	Queue[++tl]=1;
	while(hd<tl) {
		int x=Queue[++hd];
		for(re i=0; i<26; i++) {
			if(w[x].ch[i]) {
				w[w[x].ch[i]].fail=w[w[x].fail].ch[i];
				Queue[++tl]=w[x].ch[i];
			} else {
				w[x].ch[i]=w[w[x].fail].ch[i];
			}
		}
	}
	for(re i=tl; i; i--) {
		w[w[Queue[i]].fail].cnt+=w[Queue[i]].cnt;
	}
}
//#define I_love_Chtholly
int main() {
#ifdef I_love_Chtholly
	freopen("data.txt","r",stdin);
	freopen("WA.txt","w",stdout);
#endif
	n=read();
	for(re i=1; i<=n; i++) {
		scanf("%s",now);
		insert(now,i);
	}
	getfail();
	for(re i=1; i<=n; i++) {
		write(w[id[i]].cnt);
		putchar(10);
	}
	return 0;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值