AC自动机【Leo_Jose】

前置知识

Trie 字典树(必须熟练掌握才能继续往下看)
同时安利一下自己的博客
KMP(不大必要,不会KMP也可以会AC自动机,AC自动机只是借鉴了KMP的思路而已)

解决问题

给出一个长文章,一堆单词,看每个单词在文章中分别出现几次

算法思路

首先给这些待查找的字符串建立一个Trie
然后给这个Trie建立一些fail 指针,然后再匹配失败的时候沿着fail指针查找就行了

详细描述

第1步 - 构造字典树

给定5个单词 say she shr he her
然后给定一篇文章yasherhs
给这5个单词构造一个字典树
在这里插入图片描述

第2步 - 构造失配指针

概述
构造某个节点(设其为C)的失配指针方法:沿着它父亲的失配指针走,直到走到某一个节点,它的子节点中也有字母为C的节点,那么就把当前节点的失配指针指向那个字母也为C的那位儿子
如果一直走到了根节点都没找到就把失配指针指向根节点

具体操作步骤
首先将root的失配指针指向NULL
然后将root加入队列,进入循环
从队列中弹出root, root有s,h两个儿子,由于它们都是root的儿子,所以失配指针直接指向root,同时先后压入队列
这两个失配指针对应图中的(1),(2)
从队列中弹出h,h的儿子只有e,接下来找e的父亲节点h节点的失配指针所指向的节点,也就是root,由于root没有e这个儿子,并且root的失配指针指向了NULL,所以就吧e的失配指针指向root,也就是图中的(3),同时将e压入队列
将s从队列中弹出,s的儿子有a,h两个,遍历a,找到a的父亲节点的失配指针所指向的节点,也就是root,由于root没有a这个儿子,并且root的失配指针指向NULL,所以a的失配指针指向root,也就是图中的(4),同时将a压入队列
遍历节点h,寻找h的父亲节点s的失配指针所指向的节点,也就是root,由于root有h这个儿子,那么就将当前节点的失配指针指向身为root儿子的那个h,也就是图中的(5),并将h压入队列
。。。
以此类推

在这里插入图片描述
这就结束了对失配指针构建的解释

第3步 - 字符串匹配

概述
现在我们造好了一个AC自动机,开始匹配,匹配过程有两种情况
(1)当前字符匹配,那么就沿着这条路径往儿子方向走,一个个地匹配
(2)当前字符不匹配,那么就去找当前节点的失配指针所指向的节点
重复上述过程直到失配指针指向root为止

具体操作步骤
对于例子来说,模式串为yasherhs
根节点的儿子中没有ya,但是有s
s开始匹配
s有一个儿子名为h,匹配成功,继续往下
h有一个儿子名为e,我们发现e上面有一个标记(圈圈是红色的),说明e是一个单词的结尾,那么说明单词she匹配成功,
继续往下,发现e没有叫做r的儿子,那么沿着e的失配指针也就是图中的(8),找到右边的那一个e,发现这个e也是一个红色的圈圈,说明它是一个单词的结尾,也就是单词he匹配成功
继续往下,发现e中有一个叫做r的儿子,匹配成功,同时r是一个红色的圈圈,也就是说r是一个单词的结尾,单词her匹配成功
。。。
以此类推

在这里插入图片描述
这就结束了对字符串匹配的解释

代码解释

//基础定义
const int maxn=1e6+7;
struct node
{
	int son[26];//一个节点的儿子可能有a~z共26个拉丁小写字母
	int flag;//flag为几说明有几个字符串在这个节点结束
	int fail;//fail为几说明这个节点的失配指针指向几号节点
};
node trie[maxn];//trie[]数组就是一个AC自动机
char s[maxn];
queue<int>q;
int n,cnt;
//建造一个Trie
//Tire就不注释了
void insert(char* s)
{
	int u=1;
	for(int i=0;i<=strlen(s)-1;i++)
	{
		int v=s[i]-'a';
		if(!trie[u].son[v])
			trie[u].son[v]=++cnt;
		u=trie[u].son[v];
	}
	trie[u].flag++;
}
//失配指针的建立
void getFail()
{
	for(int i=0;i<=25;i++)
		trie[0].son[i]=1;//初始化
	q.push(1);//将根节点压入队列
	trie[1].fail=0;//将根节点的fail指针指向空
	while(!q.empty())
	{
		int u=q.front();
		q.pop();
		for(int i=0;i<=25;i++)
		{
			int v=trie[u].son[i];
			//处理u的i儿子的失配指针,因为这个数据结构没法返回找父亲
			int fil=trie[u].fail;
			if(!v)//如果这个节点不存在想要寻找的儿子
			{
				trie[u].son[i]=trie[fil].son[i];
				//建造失配指针
				continue;
			}
			trie[v].fail=trie[fil].son[i];//建造失配指针
			q.push(v);//v得真实存在才能压入队列
		}
	}
}
//匹配字符串
int query(char* s)
{
	int u=1,ans=0;
	for(int i=0;i<=strlen(s)-1;i++)
	{
		int v=s[i]-'a';
		int k=trie[u].son[v];//沿着失配指针跳
		while(k>1 && trie[k].flag!=-1)//如果匹配成功了
		{
			ans+=trie[k].flag;//将模式串个数记录累加上
			trie[k].flag=-1;//标记为已经经过
			k=trie[k].fail;//继续沿着失配指针跳
		}
		u=trie[u].son[v]//第一种情况,往儿子走
	}
	return ans;
}
//主程序
int main()
{
	cnt=1;//从1开始,因为根节点的编号为1
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>s;
		insert(s);
	}
	getFail();
	cin>>s;
	cout<<query(s);
	return 0;
}

练习题

LGOJ3808【模板】AC自动机(简单版)
LGOJ3796 【模板】AC自动机(加强版)
HNOI2004 L语言
NOI2011 阿狸的打字机

备注

此博客大篇幅参考了这个博客
同时代码使用的是这个博客的代码
如有侵权,请在下方评论区留言,我会将其删除

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值