AC自动机TOT


浅谈

AC自动机是啥?
是一个能够让你自动AC的算法

哦,不,是一个关于字符串匹配的算法

提到字符串匹配,大家应该都会想到KMP(既然来学AC自动机了,应该没有不知道的吧),如果你听说过KMP,你应该对一个模式串匹配一个文本串的问题了如指掌

不过,对于多个模式串,好多好多个模式串呢?

你也许会想到对每个模式串都KMP一遍,但直觉告诉你,这样是不可能AC的。于是,AC自动机就派上用场了

当然,要学习AC自动机,你还需要掌握一种数据结构——Trie树

整个AC自动机分三步:建树与插入、构建fail边和匹配查询,下面将分别讲述

》》AC自动机板子题链接


建树与插入

这个就是Trie树的基本操作啦,上个代码就好,复习一下:

void insert(char s[]){
	int len=strlen(s+1),p=0;
	for(int i=1;i<=len;i++)
	{
		char c=s[i]-'a';
		if(!trie[p].son[c])
			trie[p].son[c]=++tot;
		p=trie[p].son[c];
	}
	trie[p].cnt++;
}

我们这里没有用单词末尾的标记end,而加了一个统计单词个数的cnt,是因为题目说:数据内有重复的单词,且重复单词应该计算多次,请各位注意。


构建fail边

这一部分特别特别重要,是整个AC自动机的重点和难点所在

fail边的定义
用例子说话:首先,咱们插入这些模式串,abc,bcd,bd,c
在这里插入图片描述
那么下面我们开始匹配啦!从头开始, a , b , c , … … a,b,c,…… abc哎呀!没了!

也就是说,到c这一块,失配了。。。根据KMP的思想,我们可以接着找一个有 " b c " "bc" "bc"的地方接过去匹配

那哪里有 " b c " "bc" "bc"呢?哇! " b c d " "bcd" "bcd"这一块刚好有一个!所以我们在把 " a b c " "abc" "abc"匹配完成之后,直接跳到 " b c d " "bcd" "bcd"里的 ′ c ′ 'c' c这个节点,接着匹配就行啦

在这里插入图片描述
看到那条漂亮的紫色边没有?这就是传说中的 f a i l fail fail边,之所以叫“ f a i l fail fail边”,是因为它决定着这个字符串失配后的去向

那问题来了,最右面 " c " "c" "c"这个字符串里也有个c,为啥不和它连成 f a i l fail fail边呢?

原因很简单,公共部分"bc"比公共部分 " c " "c" "c"显然更长,咱们在 K M P KMP KMP里求的不也是最长公共前后缀嘛,也许你已经发现了,这个 f a i l fail fail边的作用,和 K M P KMP KMP中的 n e x t next next数组是一样的!!!

也就是说: f a i l fail fail边指向的就是当前节点所在的字符串的最长后缀的最后一个字符

那你来找一下 " b c d " "bcd" "bcd"中的 ′ d ′ 'd' d f a i l fail fail边指向谁呢?——答案当然是 " b d " "bd" "bd"中的 ′ d ′ 'd' d

那么 " a b c " "abc" "abc"中的 " a " "a" "a"呢?好像没有哎……没有,那就是指向根节点。
在这里插入图片描述
掌握了 f a i l fail fail边的定义,接下来我们就要开始研究代码咋写了

首先,不难发现,第一层的fail边都是根节点,然后呢?

f a i l fail fail边应该这么找:顺着你爸的fail边找上去,如果它指向的节点的孩子的字符和你的字符相等,那它的这个孩子就你要连的 f a i l fail fail

例子:

  • "abc"中的’c’的父节点是"abc"中的’b’
  • "abc"中的’b’的fail边是"bcd"中的’b’
  • "bcd"中的’b’正好有一个儿子’c’,那"abc"中的’c’就要把 f a i l fail fail边连到那儿

如果你爸没你这个孩子,那该怎么办呢?——没有孩子,咱就给它造一个,把当前的孩子直接变成你爸的fail指针的孩子,直接跳到那里去匹配。

也许你已经发现了,我们找fail边,是一层一层往下找的,所以找fail边的过程,实际上就是一个bfs的过程,需要借助队列来实现。。

void getfail(){
	queue<int>q;
	for(int i=0;i<26;i++)
	{
		int c=trie[0].son[i];
		if(c)
		{
			trie[c].fail=0;
			q.push(c);
		}
	}
	while(!q.empty())
	{
		int x=q.front();
		q.pop();
		int f=trie[x].fail;
		for(int i=0;i<26;i++)
		{
			int c=trie[x].son[i];
			if(c)
			{
				trie[c].fail=trie[f].son[i];
				q.push(c);
			}
			else trie[x].son[i]=trie[f].son[i];
		}
	}
}

匹配查询

匹配的代码,其实和 t r i e trie trie树的查找差不多,一个一个找下去,找到末尾

但这里有点不同,走到一个字符之后,咱们先去走它的 f a i l fail fail边,走完之后再继续往下找(要不咱大费周章地找 f a i l fail fail边意义何在?)

但要注意的是,题目让我们求有多少个模式串在文本串里出现过,所以出现过加完了 c n t cnt cnt之后,咱们把 c n t cnt cnt变成-1,下次遇到-1,就可以知道这个串已经统计过一遍了,就可以结束跳 f a i l fail fail的过程,去找下一个节点了

int find(char s[]){
	int x=0,sum=0,len;
	len=strlen(s+1);
	for(int i=1;i<=len;i++)
	{
		int v=s[i]-'a';
		int c=trie[x].son[v];
		while(c&&trie[c].cnt!=-1)
		{
			sum+=trie[c].cnt;
			trie[c].cnt=-1;
			c=trie[c].fail;
		}
		x=trie[x].son[v];
	}
	return sum;
}


CODE

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<iomanip>
#include<cstring>
#include<cmath>
#include<map>
#include<queue>
#define ll long long
#define ldb long double
using namespace std;

int n,tot;
char s[1001000];

struct c{
	int fail,cnt;
	int son[30];
}trie[5001000];

void insert(char s[]){
	int len=strlen(s+1),p=0;
	for(int i=1;i<=len;i++)
	{
		char c=s[i]-'a';
		if(!trie[p].son[c])
			trie[p].son[c]=++tot;
		p=trie[p].son[c];
	}
	trie[p].cnt++;
}

void getfail(){
	queue<int>q;
	for(int i=0;i<26;i++)
	{
		int c=trie[0].son[i];
		if(c)
		{
			trie[c].fail=0;
			q.push(c);
		}
	}
	while(!q.empty())
	{
		int x=q.front();
		q.pop();
		int f=trie[x].fail;
		for(int i=0;i<26;i++)
		{
			int c=trie[x].son[i];
			if(c)
			{
				trie[c].fail=trie[f].son[i];
				q.push(c);
			}
			else trie[x].son[i]=trie[f].son[i];
		}
	}
}

int find(char s[]){
	int x=0,sum=0,len;
	len=strlen(s+1);
	for(int i=1;i<=len;i++)
	{
		int v=s[i]-'a';
		int c=trie[x].son[v];
		while(c&&trie[c].cnt!=-1)
		{
			sum+=trie[c].cnt;
			trie[c].cnt=-1;
			c=trie[c].fail;
		}
		x=trie[x].son[v];
	}
	return sum;
}

int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
		scanf("%s",s+1);
		insert(s);
	}	
	getfail();
	scanf("%s",s+1);
	printf("%d",find(s));
	
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值