AC自动机详解(无指针)

1. 什么是AC自动机?

define

Aho-Corasick automation,该算法在1975年产生于贝尔实验室,是著名的多模匹配算法。——百度百科

具体来说,就是像搜索引擎一样,在搜索栏里输入几个keyword,然后在大量的网页文本里寻找这些关键字出现的次数

前决知识——KMP,trie

这不得不令人想起AC自动机的小弟,KMP算法

KMP实质上是单模匹配,一个keyword对整个文本的搜索

也正因为KMP与AC自动机关系像大哥与小弟一样,掌握AC自动机必须先掌握KMP

2. 如何实现?

在上文中已经说过了,要掌握AC自动机,首先掌握KMP和trie树(字典树)

在这里不展开讲解,简单提几句即可

(1)KMP,trie

KMP

目的:单关键词查找
本质:超级优化后的暴力
实现:维护出模式串的最长公共前后缀数组nxt,在主串和模式串一一匹配的过程中利用nxt数组实现排除重复扫描的跳跃式查找,从而提高了搜索效率

trie树

目的:用树结构来保存字符串
本质:多叉树
实现

(从 https://blog.csdn.net/Whispers_zmf/article/details/80809609 大佬那里抠的图 )

这就是一棵trie树,保存了say,shit,she,he,her,his几个单词

不要在意细节

我们定义一个空的根节点,给它26个空的子节点分别代表a~z(具体情况具体分析),然后我们将要插入的每一个字符串的首字母以ascll码代表边的标号的方式将这个字母压在边上,对于每个子节点也进行同样的操作

具体实现代码如下

#define calcid(a) a-97
int tree[maxn][26];
int flag[maxn];
inline void insert(char *s)//插入字符串s
{
	int len=strlen(s);
	int root=0;
	for(int i=0;i<len;i++)
	{
		int id=calcid(s[i]);//每一条边的编号
		if(tree[root][id]==0)//0代表节点root的id号边的节点还为空,于是为其分匹配内存
		{
			tree[root][id]=++cnt;//cnt代表着节点的编号,有点像链式前向星
			flag[cnt]=0;//flag[u]表示以u号节点所连的边代表的字符结尾的单词个数
			for(int i=0;i<26;i++)tree[tree[root][id]][i]=0;//初始化
		}
		root=tree[root][id];//将root指向下一层
	}
	flag[root]++;
}

(2)AC自动机

鸟瞰

思想:在trie树里跑KMP
核心:fail指针的构建
流程
1.建trie树
2.在trie树里构建fail指针
3.将trie树与主串匹配求解

细节
1.trie树

常规的AC自动机里的trie树与一般的trie相比没有什么太大的区别,只是要注意树上节点个数的估计和附加信息的维护(比如flag)

2.fail指针的构建

这是AC自动机的精华

目的:fail在KMP里叫做nxt(懂了吧~~)
具体定义:节点u的fail指向v,v是以u代表的字符为结尾的最长当前字符串的最长合法真后缀的最后一个节点可真绕


就像上图
找靠左的节点i的fail指针,
由于以i结尾的当前字符串(以第二层的节点开头,结尾不限)的后缀有shi,hi,i

而shi不是真后缀,hi,i不合法(不合法指没有与当前后缀完全相同的当前字符串),且hi的长度最长,因此满足要求的最长当前字符串的最长合法真后缀是hi,于是找到i的fail指针如下

当然,如果某节点的所有真后缀都非法,那么就将其指向0号节点即可

于是有下图


(第二层的s节点和第三层的e节点连向根节点 太懒不想改

构建:

仔细观察上图,我们可以发现一些有意思的规律
1.第二层的fail指针的规律
2.沿着fail指针走,深度会越来越小
3.若将fail指针和边都一视同仁的看作有向边的话,整个trie树就成了一个强联通分量,可以相互到达
4.如果a节点的fail指向了b节点,那么a节点的t儿子的fail指向b节点的t儿子(如果儿子不存在,就指根)

这些规律中,第4条对我们构建fail指针有很大帮助

据第四条,我们不难发现,只要确定了第s层的所有点的fail指针,就可以以此来确定第s+1层的所有的点的fail指针

自然而然就想到用一个bfs来维护fail指针

code:

void buildfail()
{
	queue<int>q;
	for(int i=0;i<26;i++)
	{
		if(tree[0][i])
		{
			fail[tree[0][i]]=0;
			q.push(tree[0][i]);
		}
	}//更新第二层的fail并开始bfs
	while(!q.empty())
	{
		int u=q.front();q.pop();
		for(int i=0;i<26;i++)//枚举u的每一条出边
		{
			if(tree[u][i])//如果存在
			{
				fail[tree[u][i]]=tree[fail[u]][i];//用规律4更新其fail
				q.push(tree[u][i]);
			}
			else tree[u][i]=tree[fail[u]][i];// 如果不存在这个点,我们就将这个空点连向u的fail点的i儿子
	}
}
3.匹配求解

在构建了trie树和fail指针后,是时候来求解了

如何求解?

对于未知的主串s来说,它的任何一个字母都有可能是一个模式串,因此在求解时很明显应该一个字符一个字符来求解

于是首先来一个循环,对每一个主串字符,用fail指针对这个字符的所有前缀进行一次遍历查找,统计出这个字符的答案然后累加即可

#define calcid(a) a-97
int count(char *s)
{
	int len=strlen(s);
	int now=0;//now初始是根节点
	int ans=0;//累加
	for(int i=0;i<len;i++)//对每一个字符s[i]求解
	{
		int id=calcid(s[i]);
		now=tree[now][id];
		for(int t=now;t&&~flag[t];t=fail[t])
		{
			ans+=flag[t];
			flag[t]=-1;
		}
	}
	return ans;
}

可能以下这段代码不是很好理解,因为这是fail指针的精华所在

for(int t=now;t&&~flag[t];t=fail[t])
{
	ans+=flag[t];
	flag[t]=-1;
}

由于fail指针是指向与该节点表示串后缀相等的且长度最大的串(或前缀)的节点
故这个循环其实是为了统计当前枚举到的字符是多少个当前字符串的后缀
换句话说这个过程就是在遍历当前构成的字符串的后缀中是否有其他的单词


以上就是AC自动机的基础内容,下面附上本人奇丑无比的代码
luogu模版题

#include<bits/stdc++.h>
using namespace std;
//**********************************data
#define calcid(a) a-97
const int maxn=1e6+10;
int n,cnt;
int tree[maxn][26];
int flag[maxn];
int fail[maxn];
//**********************************function
inline void insert(char *s)//插入字符串s
{
	int len=strlen(s);
	int root=0;
	for(int i=0;i<len;i++)
	{
		int id=calcid(s[i]);//每一条边的编号
		if(tree[root][id]==0)//0代表节点root的id号边的节点还为空,于是为其分匹配内存
		{
			tree[root][id]=++cnt;//cnt代表着节点的编号,有点像链式前向星
			flag[cnt]=0;//flag[u]表示以u号节点所连的边代表的字符结尾的单词个数
			for(int i=0;i<26;i++)tree[tree[root][id]][i]=0;//初始化
		}
		root=tree[root][id];//将root指向下一层
	}
	flag[root]++;
}
void buildfail()
{
	queue<int>q;
	for(int i=0;i<26;i++)
	{
		if(tree[0][i])
		{
			fail[tree[0][i]]=0;
			q.push(tree[0][i]);
		}
	}//更新第二层的fail并开始bfs
	while(!q.empty())
	{
		int u=q.front();q.pop();
		for(int i=0;i<26;i++)//枚举u的每一条出边
		{
			if(tree[u][i])//如果存在
			{
				fail[tree[u][i]]=tree[fail[u]][i];//用规律4更新其fail
				q.push(tree[u][i]);
			}
			else tree[u][i]=tree[fail[u]][i];
			// 如果不存在这个点,我们就将这个空点连向u的fail点的i儿子
		}
	}
}
int count(char *s)
{
	int len=strlen(s);
	int now=0;//now初始是根节点
	int ans=0;//累加
	for(int i=0;i<len;i++)//对每一个字符s[i]求解
	{
		int id=calcid(s[i]);
		now=tree[now][id];
		for(int t=now;t&&~flag[t];t=fail[t])
		{
			ans+=flag[t];
			flag[t]=-1;
		}
	}
	return ans;
}
//**********************************main
int main()
{
   //freopen("datain.txt","r",stdin);
   //freopen("dataout.txt","w",stdout);
   char s[maxn];
   //memset(tree,-1,sizeof(tree));
   //memset(flag,0,sizeof(flag));
   cnt=0;
   scanf("%d",&n);
   memset(tree[0],0,sizeof(tree[0]));
   for(int i=0;i<n;i++)
   {
   		scanf("%s",s);
   		insert(s);
   }
   buildfail();
   scanf("%s",s);
   printf("%d",count(s));
   return 0;
}
/********************************************************************
   ID:Andrew_82
   LANG:C++
   PROG:Aho-Corasick automation
********************************************************************/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AndrewMe8211

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值