【AC自动机】洛谷P3808 【模板】AC自动机(简单版)

题目链接:https://www.luogu.org/problem/P3808

AC自动机是看http://blog.c0per.org/2018-10/ac/https://www.luogu.org/blog/juruohyfhaha/ac-zi-dong-ji看懂的,理解怎么操作不难,但是最主要要理解的是为什么这样子操作。

首先AC自动机是在字典树的基础上加了一些类似kmp的思想,但是kmp是相同前后缀,这里的失配指针是相同后缀,kmp用next指针进行跳转,AC自动机是用一个失配指针fail进行跳转。

构建失配指针的操作:

 对于节点i,找它父亲节点的fail指针指向的那个节点j,看j的子节点中有没有和i节点相同的:

     如果有,i节点的failz指针就指向j的那个子节点;

     如果没有,继续找节点j的父亲节点的失配指针指向的节点循环

然后就是为什么要这么操作,构建完后的树是这样子的:

假如模式串是shey,以第3个节点为例:

当我们遍历到第3个节点时,到目前位置都匹配成功,并且节点3是第一个串的末尾,cntword[3]的值不为0,所以ans得到增加;然后就上跳到它的fail指针5,5是3的失配节点意味着,第二个串在第5个节点之前都是第一个串到第3个节点的后缀,也就是第二个串的he是第一个串she的后缀,那么she已知匹配,he就一定能匹配成功,但5不是第二个串的末尾,cntword[5]=0,ans没有变化;然后继续跳到5的失配节点7,第3个串到第7个节点也一定是匹配的,且7是第三个串的末尾,ans得到增加。

然后到了下一层,因为第3个节点实际上是没有y这个子结点的,但是因为它的失配指针指向的节点5有y这个子结点,所以我们在构建失配指针时,就已经将第3个节点的y子结点指向了节点6,同理这样子跳转过来本身就已经保证了节点6之前都是匹配成功的。

#include<iostream>
#include<cstring>
#include<string>
#include<queue>
using namespace std;
const int maxn=1e6+10;
int cnt=0;
int trie[maxn][26];//trie[i][j]表示的当前字母为i+'a',且父节点编号为i的节点的编号
int cntword[maxn];
int fail[maxn];
void insert(string s)
{
	int root=0;
	for(int i=0;i<s.size();i++)
	{
		int next=s[i]-'a';
		if(!trie[root][next])
			trie[root][next]=++cnt;
		root=trie[root][next];
	}
	cntword[root]++;//当前节点的单词数+1,而不是等于1,因为可能重复。
	//这时的root是单词的末尾的编号,表示的是一整个单词,途中经历过的点的cntword是没有自增的;
}
void getFail()//层序遍历
{
	queue<int>q;
	for(int i=0;i<26;i++)
		if(trie[0][i])//首先要先将最上面一层出现的字母(每个串的首字符)入队
		{
			fail[trie[0][i]]=0;
			q.push(trie[0][i]);
		}
	while(!q.empty())
	{
		int now=q.front();
		q.pop();
		for(int i=0;i<26;i++)
		{
			if(trie[now][i])//通过枚举26个字母来找到now的子节点们
			{
				//该节点i的失败指针指向它父节点now的失败指针fail[now]的和i节点相同的节点
				fail[trie[now][i]]=trie[fail[now]][i];
				q.push(trie[now][i]);
			}
			else
				trie[now][i]=trie[fail[now]][i];//如果不存在这个节点就上跳到失配节点的等于i的这个子节点上
		}
	}
}
int query(string s)
{
	int now=0,ans=0;
	for(int i=0;i<s.size();i++)
	{
		now=trie[now][s[i]-'a'];
		for(int j=now;j&&cntword[j]!=-1;j=fail[j])//当前节点匹配完成或失配时就上跳fail指针
		{
			ans+=cntword[j];//只有j是某个单词末尾时才有值
			cntword[j]=-1;//记录过的节点要标记,防止重复
		}
	}
	return ans;
}
int main()
{
	int n;
	while(cin>>n)
	{
		cnt=0;
		memset(cntword,0,sizeof(cntword));
		memset(fail,0,sizeof(fail));
		memset(trie,0,sizeof(trie));
		string a,s;
		for(int i=1;i<=n;i++)
		{
			cin>>a;
			insert(a);
		}
		getFail();
		cin>>s;
		cout<<query(s)<<endl;
	}
	return 0;
}

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值