AC 自动机学习笔记

P3808 AC 自动机(简单版)

给定 nn 个模式串 tit_i 和一个文本串 ss,求出 ss 中有多少个 tit_i 出现了。

考虑先将所有模式串插入到一棵 Trie 树中,然后在上面跑(不停从点 uu 走到 uu 的边权为 sis_i 的儿子,即令 uu 变为 sonu,sison_{u,s_i})。这样若当前点 uu 为某个 tit_i 对应的点(可以在插入时给这个点打标记),则说明 tit_iss 中出现了。

但是这样不停跑可能会出现这样的情况:当前位于点 uu,而 sonu,si=0son_{u,s_i}=0 即不存在对应的儿子,此时我们无法直接走到 sonu,sison_{u,s_i}。不妨称这种情况为“失配”。

解决办法很简单,假设第一次出现这种情况时跑完了 s[1,i]s_{[1,i]},当前在点 uusonu,si+1=0son_{u,s_{i+1}}=0。则此时 uu 对应的字符串是 s[1,i]s_{[1,i]}。为了能继续往下跑,我们可以跳到一个 Trie 树上的节点 vv,使得 vv 代表的字符串是 s[1,i]s_{[1,i]} 的后缀,且 v=uv\not=u

如果这样的 vv 有很多个,那么跳到最深(代表的字符串最长)的那个一定是最优的(因为从它出发能跳到其它合法的 vv)。并且由于 Trie 中每个点的字符串两两不同,故最深的 vv 一定只有一个。

不妨令 failufail_u 为最深的这样的 vv,即对应字符串是点 uu 对应字符串的后缀且深度最大的点 vv(需满足 v=uv\not=u)。

考虑 failfail 的求解。注意到 failufail_u 的深度一定小于 uu 的深度,故可以使用 bfs 求解。先将 fail0fail_0 以及所有 00 的儿子的 failfail 设为 00。对于一个点 uuv=sonu,i=0v=son_{u,i}\not=0

  • p=failup=fail_u 并不断令 p=failpp=fail_p,直到:
    • p=0p=0,则令 failv=0fail_v=0
    • sonp,i=0son_{p,i}\not=0,则令 failv=sonp,ifail_v=son_{p,i}

暴力跳太慢了,不妨在 sonu,i=0son_{u,i}=0 时令 sonu,i=sonfailu,ison_{u,i}=son_{fail_u,i},这样就不用暴力跳了:

  • sonu,i=0son_{u,i}\not=0,则令 failsonu,i=sonfailu,ifail_{son_{u,i}}=son_{fail_u,i}

sonu,ison_{u,i} 实际上变成了:

  • uu 出发,不断令 u=failuu=fail_u 直到边权为 ii 的儿子 vv 存在,sonu,ison_{u,i} 就为这个 vv

failfail 数组的代码如下:

inline void build()
{
	queue<int> q; 
	fail[0]=0;
	for(int i=0;i<26;i++)
	{
		int v=son[0][i];
		if(v!=0) fail[v]=0,q.push(v);
	}
	while(!q.empty())
	{
		int u=q.front();
		q.pop();
		for(int i=0;i<26;i++)
		{
			int v=son[u][i];
			if(v!=0) fail[v]=son[fail[u]][i],q.push(v);
			else son[u][i]=son[fail[u]][i];
		}
	}
}

对于例题,不难发现当在点 uu 时,实际上能匹配 u,failu,failfailu,u,fail_u,fail_{fail_u},\dots 对应的字符串,即不断跳 failfail 经过的点都能匹配到。故还需记录一个点是否被跳过了,如果经过跳过的点就停止跳 failfail

inline int calc(char* s,int n) // sum[u] 表示有多少个文本串等于 u 表示的字符串
{
	int u=0,res=0;
	for(int i=1;i<=n;i++)
	{
		int id=s[i]-'a'; 
		u=son[u][id];
		int v=u;
		while(v!=0&&sum[v]!=-1) // 跳到根节点或者跳到过就不用跳 
		{
			res+=sum[v]; // 计算贡献 
			sum[v]=-1;   // 标记跳到过了 
			v=fail[v];   // 继续跳 fail 
		}
	}
	return res;
}

实际上,由于每个点只有一个 failfail,且不断跳 failfail 必定停留在点 00,故 failfail 实际上是一棵根为 00,包含所有 Trie 中节点的树,即 fail 树。

练习: