AC自动机·第一篇(大概有后续)

最近老师呢,让我们做了一套题,然后里面有一道可持久化trie树,蒟蒻不会。蒟蒻呢,于是想去学。但是可持久化还是太早了,于是从ac自动机开始!

AC自动机嘛……有点类似于trietree上的KMP。

前置知识,字典树   ,kmp(其实知道kmp思想就好啦)

其实呢,蒟蒻tsz原本是不会ac自动机的,但是看着别人的博客去学了。最后学会了,于是想写一篇博客。

首先本人在洛谷上做了两道题,坐标:   简单版  ,   加强版。然后开始讲课!

我们看过题后发现,如果每个模式串都去和文本串匹配一遍的话,哪怕用kmp,或者只用字典树的话,肯定会TLE。

照着正常的字典树打,我们可以考虑加上一个叫做“fail”的指针,指向的是上一个可以匹配的地方。

比如说这里有一个字典树:

root为根节点,接下来每一个节点都存一个字符,这就是正常的字典树。

然后我们在看一看加了fail指针的字典树:

PS:没有加箭头,fail指针的方向是向上的,自行脑补。

 红色的是fail指针,现在能不能找到一些些规律?

最简单,最显而易见的是:每一个节点的fail指针都是指着和当前节点代表字符相同的节点(或者是root)。

其实,还有一个规律:每一个节点的fail指针,指的是它父亲的fail指针的对应子节点。

为什么是这样?为什么要设fail指针?别急我们慢慢来看~

首先我们看到这个fail指针:

对于这个abc→bc的fail指针我们可以发现它是从ab→b推导过来的。然后可以发现fail指针所指的字符串是当前字符串的后缀

(abc指的是路径root→a→b→c的最后一个节点,bc指的是路径root→b→c的最后一个节点,下文同)

当我们做完trie树和fail指针之后,接下来肯定是查询,这时候我们只会用到我们的文本串。

假设对于上图字典树,我们有一个文本串‘abcd’,那么首先它肯定会访问root,接下来走到a,由于a的fail指针指向的是root,没有意义,所以此次被查询的只有a。

于是往下走到ab。ab有一个fail指针,指向的是b。b指向的是root,没有意义。于是此次查询的是ab和b两个节点。

然后继续往下走,走到abc,abc有一个fail指针,指向的是bc。bc也有个指针,指向的是c。然而c指向的是root,没有意义。于是本次操作查询的是abc,bc,c,这三个节点。

之后问题就来了:那么按道理,我之后应该走的是abcd这一个节点啊,但是我没有这个节点,怎么办?

这里有一个非常奇妙的方法:当不存在这个子节点的话,当前节点就会把它fail指针的对应子节点当做自己的子节点。

abc没有自己的'd'子节点,于是它就把bc的'd'子节点当做自己的子节点,但是bc原本也没有子节点,它的‘d’子节点本来就是c的‘d’子节点cd。于是abc的‘d’子节点就是cd了!

所以最后一步文本串往下走,也就是走到cd节点,重复之前的操作,查询到了cd和d节点。

然后我们就知道最后的答案啦~

那怎么做fail指针呢?有种东西叫bfs,具体看代码:

void trie_fail()
{
	queue<int>q;
	for(int i=0;i<26;i++)
	{
		if(ac[0].nxt[i]!=0)
		{
			ac[ac[0].nxt[i]].fail=0;
			q.push(ac[0].nxt[i]);
		}
	}//root的直接子树先预处理
	while(!q.empty())
	{
		int x=q.front();
		q.pop();
		for(int i=0;i<26;i++)
		{
			if(ac[x].nxt[i]!=0)//如果存在这个子节点
			{
				ac[ac[x].nxt[i]].fail=ac[ac[x].fail].nxt[i];
                //子节点的fail等于当前节点fail的对应子节点
				q.push(ac[x].nxt[i]);//压进队列
			}
			else ac[x].nxt[i]=ac[ac[x].fail].nxt[i];
            //如果不存在这个子节点,那么子节点就等于当前节点fail的对应子节点
		}
	}
}

顺便贴一张luogu【p3796】的代码:

#include<bits/stdc++.h>
#define maxn 100001
using namespace std;
struct trie
{
	int nxt[26],fail,num;
}ac[maxn];
int n,cnt,sum,ans[maxn];
char s[1000001],ch[210][110];
void clan()
{
	for(int i=0;i<=cnt;i++)
	{
		memset(ac[i].nxt,0,sizeof(ac[i].nxt));
		ac[i].num=ac[i].fail=0;
	}	
	cnt=0;sum=0;
	for(int i=1;i<=n;i++)ans[i]=0;
}
void trie_ins(int i)
{
	int root=0,len=strlen(ch[i]+1);
	for(int j=1;j<=len;j++)
	{
		if(!ac[root].nxt[ch[i][j]-'a'])ac[root].nxt[ch[i][j]-'a']=++cnt;
		root=ac[root].nxt[ch[i][j]-'a'];
	}
	ac[root].num=i;//题目保证了每一个模式串不一样,直接记录一下编号就好
}
void trie_fail()
{
	queue<int>q;
	for(int i=0;i<26;i++)
	{
		if(ac[0].nxt[i]!=0)
		{
			ac[ac[0].nxt[i]].fail=0;
			q.push(ac[0].nxt[i]);
		}
	}
	while(!q.empty())
	{
		int x=q.front();
		q.pop();
		for(int i=0;i<26;i++)
		{
			if(ac[x].nxt[i]!=0)
			{
				ac[ac[x].nxt[i]].fail=ac[ac[x].fail].nxt[i];
				q.push(ac[x].nxt[i]);
			}
			else ac[x].nxt[i]=ac[ac[x].fail].nxt[i];
		}
	}
}
void quest()
{
	int root=0,len=strlen(s+1);
	for(int i=1;i<=len;i++)
	{
		int v=s[i]-'a';
		int k=ac[root].nxt[v];
		while(k>=1)//k=0的话就访问到root了
		{
			if(ac[k].num!=0)ans[ac[k].num]++;
			k=ac[k].fail;
		}
		root=ac[root].nxt[v];
	}
}
int main()
{
	while(1)
	{
		scanf("%d",&n);
		if(n==0)return 0;
		clan();//初始化,函数名随便取的
		for(int i=1;i<=n;i++)
		{
			scanf("%s",ch[i]+1);
			trie_ins(i);//插入
		}
		trie_fail();//建立fail指针
		scanf("%s",s+1);
		quest();//查询
		int maxx=0;
		for(int i=1;i<=n;i++)maxx=max(maxx,ans[i]);
		printf("%d\n",maxx);
		for(int i=1;i<=n;i++)
		{
			if(maxx==ans[i])
			{
				int len=strlen(ch[i]+1);
				for(int j=1;j<=len;j++)printf("%c",ch[i][j]);
				printf("\n");
			}
		}
	}
}

最后来一个小插曲/番外/小故事:

tsz前天AC了ac自动机加强版并从此学会了ac自动机。但是之后有碰到了ac自动机的简单版,然后没有过样例。最后用样例又hack了加强版的模板框架,发现自己是while(k>=1)打成了while(k>1),大呼:“数据好水”。

最后希望大家学完新算法要多刷同类型题,不仅是巩固知识,也可以检验一下模板的准确性。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值