AC自动机 学习笔记

前置知识

  • Trie 树
  • KMP 的思想(也许不用?)

AC自动机是什么?

可以帮你自动A题的作弊玩意

AC 自动机是 以 Trie 的结构为基础,结合 KMP 的思想 建立的。
简单来说,建立一个 AC 自动机有两个步骤:
1.基础的 Trie 结构:将所有的模式串构成一棵 Trie。
2.KMP 的思想:对 Trie 树上所有的结点构造失配指针。
然后就可以利用它进行多模式匹配了。
——OI-Wiki

模板题

https://www.luogu.com.cn/problem/P3808
给定 n n n 个模式串 s 1 , … , n s_{1,\dots ,n} s1,,n 和一个文本串 t t t,求有多少个不同的模式串在文本串里出现过。
两个模式串不同当且仅当他们编号不同。

1 ≤ n ≤ 1 e 6 1 \leq n \leq 1e6 1n1e6 1 ≤ ∣ t ∣ ≤ 1 e 6 1 \leq |t| \leq 1e6 1t1e6 1 ≤ ∑ i = 1 n ∣ s i ∣ ≤ 1 e 6 1 \leq \sum \limits_{i=1} ^n |s_i| \leq 1e6 1i=1nsi1e6

算法流程

对于上面这道题,显然我们可以对于每个模式串都判断一次是否在文本串里,时间复杂度 O ( n ⋅ ∣ t ∣ ) O(n · |t|) O(nt)。很明显过不了。

这时,我们想到:KMP 算法中,我们利用了失配指针减少了不必要的比较,提高了算法效率!这种想法是否也可以用在这一题里呢?

显然是可以的。

考虑两个模式串abcdbce,文本串为abce

对于第一个模式串,当我们判断到文本串e这个字符时,发现不对了。如果按照上面的思路,我们需要从头再枚举,判断第二个模式串是否出现。但是,我们观察到,bce的前两个字符在判断一个模式串时就已经被比较过了,产生了不必要的比较。因此,我们可以用一个失配指针,从第一个模式串的c指向第二个模式串的c

存储模式串,我们使用 Trie 树。

通过上面的这个例子,我们可以发现:当一个模式串的某一前缀为另一模式串的子串时,可以用上失配指针。

具体地说,若 S i ( 1 , k ) = S j ( x , x + k − 1 ) S_i(1,k) = S_j(x,x+k-1) Si(1,k)=Sj(x,x+k1),就产生一个从 S j [ x + k − 1 ] S_j[x+k-1] Sj[x+k1] S i [ k ] S_i[k] Si[k] 的失配指针,因为当 S j ( 1 , x + k − 1 ) S_j(1,x+k-1) Sj(1,x+k1) 在文本串中时, S j ( x , x + k − 1 ) = S i ( 1 , k ) S_j(x,x+k-1)=S_i(1,k) Sj(x,x+k1)=Si(1,k) 也一定在文本串中,我们无需比较 S i S_i Si 的前 k k k 位。

通过一段分析,我们已经大致得出了算法的思路:存模式串,建失配指针,查找。

接下来就只剩细节问题了。

失配指针的建立

我们使用 BFS 建立失配指针。

来看看这样的一个 Trie 树。

(Trie 树的所有节点都代表了一个字符串,所有边都代表了一个字符)

(图待补充)

很明显,对于第一层的所有节点,失配指针只能指向根节点。

对于第二层及以下的层:

设当前正在考虑的节点为 i i i,它代表的字符串为 t r e e [ i ] tree[i] tree[i],其父亲为 f a [ i ] fa[i] fa[i] x x x 的失配指针指向的节点为 f a i l [ x ] fail[x] fail[x],连接 x x x y y y 的边代表的字符为 c ( x , y ) c(x,y) c(x,y)

根据失配指针的定义, t r e e [ f a [ i ] ] ( x 0 , x 0 + k − 1 ) = t r e e [ f a i l [ f a [ i ] ] ] ( 1 , k ) tree[fa[i]](x_0,x_0+k-1)=tree[fail[fa[i]]](1,k) tree[fa[i]](x0,x0+k1)=tree[fail[fa[i]]](1,k)

i i i 存在:
f a i l [ f a [ i ] ] fail[fa[i]] fail[fa[i]] 的一个儿子 r r r 满足 c ( f a [ i ] , i ) = c ( f a i l [ f a [ i ] ] , r ) c(fa[i],i)=c(fail[fa[i]],r) c(fa[i],i)=c(fail[fa[i]],r),则 t r e e [ i ] ( x 0 , x 0 + k ) = t r e e [ r ] ( 1 , k + 1 ) tree[i](x_0,x_0+k)=tree[r](1,k+1) tree[i](x0,x0+k)=tree[r](1,k+1),即 i i i 的失配指针应指向 r r r。否则 i i i 的失配指针指向根节点。同时将 i i i 入队。

i i i 这个节点不存在,则给 f a [ i ] fa[i] fa[i] f a i l [ f a [ i ] ] fail[fa[i]] fail[fa[i]] 的满足 c ( f a [ i ] , i ) = c ( f a i l [ f a [ i ] ] , r ) c(fa[i],i)=c(fail[fa[i]],r) c(fa[i],i)=c(fail[fa[i]],r) 的儿子 r r r 连一条边。

这样,经过一次 BFS,失配指针就建立好了。

查找答案

和暴力的思路差不多,在 Trie 树中,一个一个字符查找文本串,如果到了一个模式串的末尾就更新答案,如果匹配不上就跳失配指针。

代码

#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
using namespace std;

int n;
char s[1000010];
int tr[1000010][30],tg[1000010],fail[1000010];
int cnt;

void add(char s[])
{
	int nw = 0,len = strlen(s);
	for (int i = 0;i < len;i ++)
	{
		if (!tr[nw][s[i] - 'a'])
			tr[nw][s[i] - 'a'] = ++ cnt;
		nw = tr[nw][s[i] - 'a'];
	}
	tg[nw] ++;
}

queue <int> q;
void build()
{
	for (int i = 0;i < 26;i ++)
		if (tr[0][i]) q.push(tr[0][i]);
	while (q.size())
	{
		int nw = q.front();
		q.pop();
		for (int i = 0;i < 26;i ++)
			if (tr[nw][i])
				fail[tr[nw][i]] = tr[fail[nw]][i],q.push(tr[nw][i]);
			else tr[nw][i] = tr[fail[nw]][i];
	}
}

void query(char s[])
{
	int nw = 0,ans = 0,len = strlen(s);
	for (int i = 0;i < len;i ++)
	{
		nw = tr[nw][s[i] - 'a'];
		for (int j = nw;j && tg[j] != -1;j = fail[j])
		{
			ans += tg[j];
			tg[j] = -1;
		}
	}
	printf("%d\n",ans);
}

int main()
{
	scanf("%d",&n);
	for (int i = 1;i <= n;i ++)
	{
		scanf("%s",s);
		add(s);
	}
	build();
	scanf("%s",s);
	query(s);
}

例题

https://www.luogu.com.cn/problem/P3808(模板)https://www.luogu.com.cn/problem/P3796(加强版)
(待补充)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值