AC 自动机

比我小一届却吊打我的大脚玩家(djwj233)的博客

AC 自动机内容与含义

AC 自动机(Aho-Corasick automaton,abbr. ACAM),诞生于贝尔实验室。

两个定义:

  • 文本串:匹配别人、包含其他串的串。
  • 模式串:被匹配、被包含的串。

AC 自动机是一种多模匹配算法,就是解决多个模式串匹配单个/多个文本串用的。

总的来说,AC 自动机类似将所有串变成一个连接所有串的 KMP。

AC 自动机由 Trie 树、Fail 树和连接 Trie 和 Fail 的边组成。

  • Trie 树上的边表示一个串后面接上一个字符的前缀关系。
  • Fail 树上的边表示一个串前面接上一个串的后缀关系,即失配关系。
  • 连接 Trie 和 Fail 的边表示在不同串之间的的失配关系。

通过 Trie 树和 Fail 树,AC 自动机可以用来为多个串同时匹配。一般我们先把最长的能够匹配的模式串(在 ACAM 上找出来)求出来,那么它沿着 Fail 树向上的所有串都被匹配了一次。

AC 自动机建立

首先对于所有模式串建立 Trie 树,找到他们的 Fail 指针,代码如下:

vector<int> Fail_t[Maxn];
int ch[Maxn][26],Fail[Maxn]/*,cnt[Maxn]*/;
inline int Insert(char s[])
{
	 int len=strlen(s+1),x=0;
	 for(int i=1;i<=len;i++)
	 {
	 	 if(ch[x][s[i]-'a']) x=ch[x][s[i]-'a'];
	 	 else ch[x][s[i]-'a']=++tot,x=tot;
	 }
	 /*cnt[x]++;*/ return x;
}
inline void get_fail()
{
	 queue<int> q;
	 for(int i=0;i<26;i++) if(ch[0][i]) q.push(ch[0][i]);
	 while(!q.empty())
	 {
	 	 int cur=q.front(); q.pop();
	 	 Fail_t[Fail[cur]].pb(cur);
	 	 for(int i=0;i<26;i++)
	 	 {
	 	 	 if(ch[cur][i]) q.push(ch[cur][i]),
	 	 	 	 Fail[ch[cur][i]]=ch[Fail[cur]][i];
	 	 	 else ch[cur][i]=ch[Fail[cur]][i];
		 }
	 }
}

但有点时候如果字符集太大了怎么办?用 map

假设如P2336 [SCOI2012]喵星球上的点名,字符总长 \(10^5\),字符集 \(10^4\),我们用了 map 也不能够记完所有 Trie 与 Fail 的连接,那就只能舍去这些边,只建出 Trie 和 Fail 树。

map<int,int> Trie_t[Maxc];
vector<int> Fail_t;
void get_fail()
{
	 queue<int> q;
	 for(auto v:Trie_t[0]) q.push(v.fi);
	 while(!q.empty())
	 {
	 	 int cur=q.front(); q.pop();
		 Fail_t[Fail[cur]].pb(cur);
		 for(auto v:Trie_t[cur])
		 	 q.push(v.se),Fail[v.se]=Trie_t[cur][v.fi];
	 }
}

AC 自动机上的串匹配

需要注意的是,ACAM 中的串都是模式串。

P5357 【模板】AC 自动机(二次加强版)

给定 \(n\) 个模式串 \(T_i\) 和文本串 \(S\),求每一个模式串 \(T_i\)\(S\) 中的出现次数。

\(n\le 2\times 10^{5},|S|\le 2\times 10^{6},\sum{|T_i|}\le 2\times 10^{5}\)

直接给 \(T\) 建立 ACAM,\(S\) 从根开始,每走到一个节点那么这各节点到根的路径上的每一个节点(都是一个串)都在 \(S\) 中出现一次,最终每个节点上的数值就是出现次数。

由于每次都从这个节点到根标记会非常慢,所以在每个节点上标记,最终查询子树中标记个数就是出现次数。

P2414 [NOI2011] 阿狸的打字机

给定 \(n\) 个串 \(T_i\),有 \(m\) 次询问,每次询问第 \(i\) 个串在第 \(j\) 个串中的出现次数。

\(n,m\le 10^{5},\sum{|T_i|}\le 10^{5}\)

每次将 \(i\) 当做模式串,将 \(j\) 当做文本串,假设只有一次询问,我们就会仿照上一题的套路从根开始一路标记到 \(j\) 节点。由于这个题比较特殊,\(j\) 是 ACAM 上实际存在的点,只会经过 Trie 树上的边走到,所以一路标记下来恰好是根到 \(j\) 的一条路径。

询问的时候直接查询以 \(i\) 为根的子树和即可。

如果有多次询问?可以先把询问挂到文本串下面,以 dfn 序遍历 Trie 树,每次询问以 \(i\) 为根的子树和即可(树状数组维护)。

AC 自动机上的 DP

一般都是设 \(dp(i,j,\dots)\) 表示已经接了长度为 \(i\),当前走到第 \(j\) 个节点的状态及信息,有时还需要记下其他信息。

P4052 [JSOI2007]文本生成器

正难则反,之后仿照上面的 dp 格式基本上就做完了。

P3311 [SDOI2014] 数数

直接把数位 DP 扔上去就好了。

AC 自动机杂题

P2336 [SCOI2012]喵星球上的点名

其中有一个常见技巧:

给定 \(n\) 个模式串 \(T_i\)\(m\) 个文本串 \(S_i\),求出每个串 \(T_i\) 在多少个串 \(S_i\) 中出现过。

直接查询 \(T_i\) 的子树和会导致算重,所以对于串 \(S_i\) 走到的节点序列 \(\{a\}\),按照 dfn 序排序,在相邻两个数的 \(Lca\) 处减去贡献,可以避免算重的情况。

posted @ 2022-01-27 20:06  EricQian06  阅读(89)  评论(0编辑  收藏  举报