AC 自动机

理论

不懂自动机相关理论,直观感受就是在 Trie 树上的 KMP,用来解决多个字符串匹配的问题。

与 KMP 一样,我们在 Trie 树上的每个节点记录一个 \(fail\) 指针,假设当前节点为 \(i\),Trie 树上对应的前缀为 \(s\),若存在 \(s\) 的最长后缀 \(t\),满足 \(t\) 也在 Trie 树上出现,则 \(fail\) 指针即指向这个 \(t\) 在 Trie 树上代表的节点。

如何构建 \(fail\) 指针?下文记 \(c\) 儿子为当前节点对应字符串加入字符 \(c\) 后得到的新字符串在 Trie 上对应的几点。考虑节点 \(x\) 若存在 \(c\) 儿子为 \(y\),目标是求出 \(fail_y\),且比 \(y\) 深度小的节点 \(fail\) 指针均构建完成。可以通过不断让 \(x\gets fail_{x}\),直到 \(x'\)\(c\) 儿子,则 \(fail_y\) 即为此时 \(x'\)\(c\) 儿子。

如何优化这个过程?暴力跳 \(fail\) 是瓶颈,容易发现可以给这个过程记忆化,设 \(f_{i,j}\) 表示 Trie 树上节点 \(i\) 对应的字符串加入字符 \(j\) 后(与 \(j\) 儿子定义不同,这个不一定存在)形成的新字符串 \(s\),满足同时出现 Trie 树上的 \(s\) 的最长后缀对应的节点。若 \(i\)\(j\) 儿子,则 \(f_{i,j}\) 就是 \(i\)\(j\) 儿子,若没有,则 \(f_{i,j}=f_{fail_i,j}\)。同理,若 \(i\)\(j\) 儿子,则 \(fail_{f_{i,j}}=f_{fail_i,j}\),这个类似递推的过程实际上相当于将上面暴力匹配的过程记忆化了。

按照深度从小到大构建 \(f_{i,j}\)\(fail_i\) 即可,可以用 bfs 实现,记 \(L\) 为字符串总长。则搭建这些字符串对应 AC 自动机的时间复杂度 \(O(L)\),有一个字符集大小的常数,某些特定题目中会去优化掉这一部分常数(例如字符串长度很小但字符集很大)。

void build(){
    queue<int>q;
    for(int i=0;i<26;i++){
        fail[tr[0][i]]=0;
        if(tr[0][i])q.push(tr[0][i]);
    }
    //上面的步骤相当于初始化,不能省
    while(q.size()){
        int x=q.front();q.pop();
        for(int i=0;i<26;i++){
            if(!tr[x][i])tr[x][i]=tr[fail[x]][i];
            else{
                fail[tr[x][i]]=tr[fail[x]][i];
                q.push(tr[x][i]);
            }
        }
    }
    return;
}

我们将原来存在于字典树的边称为树边,将为了辅助而算出的 \(f_{i,j}\) 为非树边。而 \(fail_x\) 因为只会指向深度比 \(x\) 小的点,所以所有 \(fail\) 指针构成的就是一颗树,被称为 \(fail\) 树,能够把字符串匹配问题变成树上问题。

例题

P3808 AC 自动机(简单版)

\(n\) 个模式串建出 AC 自动机,然后在 AC 自动机上跑一遍 \(t\)。假设跑完 \(t\) 的某个前缀后走到了节点 \(x\),通过 \(x\) 不断跳 \(fail\) 直到根,路过的所有节点对应的字符串均是 \(t\) 某个前缀的后缀(即子串),插入 \(s_i\) 时打上标记,若路过的节点有标记,则对答案加上贡献,并将标记清空,防止重复计算。因为一个节点只会跳一遍,所以时间复杂度 \(O(L)\)

int solve(int len){
    int ans=0,p=0;
    for(int i=1;i<=len;i++){
        int ch=s[i]-'a';
        p=tr[p][ch];
        for(int j=p;mk[j]!=-1;j=fail[j]){
            ans+=mk[j];
            mk[j]=-1;
        }
    }
    return ans;
}

P3796 AC 自动机(简单版 II)

还是跑一遍 AC 自动机,按照上面的流程给 \(x\) 跳到的每个节点打标记,最后看那个字符串末尾的节点标记数最多即可,因为一个节点可能跳多次,所以时间复杂度为 \(O(TL^2)\)

显然过不了,考虑优化,发现对于文本串 \(t\) 的每个前缀对应的节点 \(x\),暴力跳 \(fail\) 相当于将 \(x\) 到根这条路径上所有点都打上一个标记,借助差分思想,可以先只在 \(x\) 打标记,最后在 \(fail\) 树上 DFS 一遍求出每个节点 \(fail\) 树子树内有多少个标记,这个就是我们求的匹配次数,时间复杂度 \(O(TL)\)

P5357 【模板】AC 自动机

和上面题几乎一样。

P3121 [USACO15FEB] Censoring G

通过 \(fail\) 树上前缀和,可以快速算出当前前缀是否有后缀匹配上了模式串,如果有,就消掉,可是消掉这个部分后匹配的指针需要指向哪里?这个可以用数组记录,若当前匹配了 \(x\) 个字符,要消掉 \(y\) 个字符,则就让指针指向数组中 \(x-y\) 的位置,然后将这 \(y\) 个字符删除即可,每次指针移动再记录到数组中。时间复杂度 \(O(L)\)

P4052 [JSOI2007] 文本生成器

比较经典的 AC 自动机上 dp。对于一个字符串,可以分为两类,出现过模式串和没出现过模式串,由此设计出 dp,设 \(f_{i,j,0/1}\) 表示若字符串匹配了前 \(i\) 个字符后走到节点 \(j\),且当前这个字符串有没有出现过模式串的方案数,预处理出某个节点是否存在后缀为模式串(即跳 \(fail\) 指针能不能跳到模式串的结尾点)。从前往后推,假设 \(f_{j,c}=k\),分类讨论:

\(k\) 存在后缀为模式串,则 \(f_{i+1,k,1}\gets f_{i,j,1}+f_{i,j,0}\)

反之,则 \(f_{i+1,k,1}\gets f_{i,j,1},f_{i+1,k,0}\gets f_{i,j,0}\)

记得取模,时间复杂度 \(O(mL)\)

int DP(){
	dp[0][0][0]=1;
	for(int i=0;i<k;i++){
		for(int j=0;j<=idx;j++){
			for(int c=0,nxt;c<26;c++){
				nxt=tr[j][c];
				dp[i+1][nxt][0]+=dp[i][j][0];
				if(mk[nxt])dp[i+1][nxt][1]+=dp[i][j][0];
				else dp[i+1][nxt][1]+=dp[i][j][1];
				dp[i+1][nxt][0]%=mod,dp[i+1][nxt][1]%=mod;
			}
		}
	}
	for(int i=0;i<=idx;i++){
		ans=(ans+dp[k][i][1])%mod;
	}
	return ans;
}

P3041 [USACO12JAN] Video Game G

蒟蒻的第一道 AC 自动机上 dp。容易求出每个后缀 \(i\) 有多少个前缀为模式串,且这些模式串的权值和也可以前缀和求出,和上题一样设计状态即可,时间复杂度 \(O(kL)\)

P3311 [SDOI2014] 数数

怎么还有数位 dp。

可以和数位 dp 一样,求出 \(f_{i,j}\) 表示 \(i\) 位数,不含前导 \(0\),在 AC 自动机中匹配最后走到 \(j\) 的方案数,可以和普通 dp 一样处理,若 \(j\) 对应前缀存在后缀为模式串,则算完后记得将对应状态的 dp 值清零。

计算答案,先考虑位数小于 \(|s|\) 的情况,然后数位 dp。每次都紧贴但不取上界,留到下一位,最后记得取 \(s\) 本身。时间复杂度 \(O(|s|L)\)

P2444 [POI 2000] 病毒

建出 AC 自动机后,对于每个前缀 \(i\) 求出是否存在后缀为模式串,若存在,则将无限长的串放入 AC 自动机一定不会经过这个点。

我们把所有不符合上述条件的点拿出来,并将这些点对应的边拉出来建一个子图,若子图存在环,则存在符合方案,通拓扑排序判环即可,时间复杂度 \(O(L)\)

CF1202E You Are Given Some Strings...

对于 \(s_i+s_j\)\(t\) 中出现一次,产生了一个贡献,假设分界点为 \(p\),其中 \(s_i\)\(t_{p-|s_i|+1,p}\) 匹配,\(s_j\)\(t_{p+1,p+|s_j|}\) 匹配,则让其在 \(p\) 处产生贡献,话句话说,求出 \(f_i,g_i\)\(f_i\) 为前缀 \(i\) 有多少个后缀为模式串,\(g_i\) 为后缀 \(i\) 有多少个前缀为模式串,因为前后 \(s_i,s_j\) 独立,所以最终的答案根据乘法原理为 \(\sum f_ig_{i+1}\)\(f_i,g_i\) 只需正反跑一遍多模式串匹配即可,时间复杂度 \(O(L)\)

这题 \(n\) 定义为字符类型调试 1h,望周知。

CF163E e-Government

动态加字符串,如何做?考虑离线,先将所有模式串建出 AC 自动机,对于文本串的匹配,即为匹配路径上每一个 \(x\),在 \(fail\) 树上 \(x\) 到根上的标记和,但标记是可以改变的。

考虑动态维护 \(fail\) 树,若加入一个模式串 \(s\),就在 \(fail\) 树上 \(s\) 末尾节点处打上 \(1\) 的权值标记,若删除一个模式串 \(s\),就在 \(fail\) 树上打上 \(-1\) 的标记,每个文本串匹配路径上每个点都要进行从当前点到根的链查询。

\(O(n)\) 次单点修改,\(O(L)\) 次链查询,树链剖分动态维护做到 \(O(n\log L+L\log^2 L)\),不够优秀,考虑倒过来,在树上给节点 \(x\) 打权值为 \(v\) 的标记,相当于给整颗子树加 \(v\),单点查询只需要看这个节点被打了多少次标记,用 dfs 序拍平后维护差分数组,直接用树状数组即可做到 \(O((n+L)\log L)\)

纯口胡。

CF710F String Set Queries

很好,强制在线。

一种维护的方法是根号定期重构,我们每插入 \(\sqrt{n}\) 个串就对所有的串建一次 AC 自动机,而没插入的字符串就先放着,每次查询时,与 AC 自动机的字符串先多模式串匹配,对于没来得及插入 AC 自动机的串暴力 KMP 匹配,删除操作时,对每一个插入和删除的字符串赋权值,插入的赋 \(1\),删除的赋 \(-1\),这样都转化为插入操作,时间复杂度 \(O(\sqrt{n}L)\)

不够优秀,一个更牛的做法是二进制分组。维护一堆 AC 自动机。每插入一个字符串,就让她单独建一颗 AC 自动机,若这个 AC 自动机和这一堆 AC 自动机中某个包含的串的数量一样,在将这两台 AC 自动机合并,包含串的个数累加,容易发现维护的这一堆 AC 自动机包含的串的个数都是 \(2\) 的次幂,所以这一堆 AC 自动机个数不超过 \(\log n\) 个,因此文本串匹配只需要跑 \(\log n\) 次即可。考虑合并的复杂度,每个 AC 自动机合并后包含的串的数量翻倍,但总数量不超过 \(n\) 个,所以合并次数不超过 \(\log n\),一个文本串对于 AC 自动及合并时间的贡献为 \(O(L)\),所以总复杂度为 \(O(L\log n)\)

对于 AC 自动机的合并,我们保留每个 AC 自动机的字典树部分,每次合并时合并字典树,合并后再建 AC 自动机。

[NOI2011] 阿狸的打字机

建字典树的方式很巧妙,若加入字符和普通 Trie 树一样,若删除则为回到父亲,打印即为打上标记,因此可以建出 AC 自动机。

对于询问 \((x,y)\),暴力相当于在 \(x\) 串结尾打上标记(询问完成后清楚),然后在 AC 自动机上跑一遍 \(y\) 串,路径上的每个 \(x\) 都查询当前点到根是否有标记,因为 \(y\) 串长不确定,所以复杂度没有保障。

考虑反过来,在 AC 自动机上跑一遍 \(x\),给路过的节点打标记,再查询 \(fail\) 树上 \(y\) 节点子树内有多少标记。由此引申出一个离线算法,把所有询问 \((x,y)\) 挂到 \(x\) 串的末尾节点上。 dfs 整个 AC 自动机(按照字典树)。保证当前点 \(u\) 到根路径上所有点都打上标记,可以用树状数组实现,进入 dfs 打标记,退出删标记。每走到点 \(u\),处理挂在节点上的询问,单点加,子树查,dfs 序拍平即可。时间复杂度即为 \(O(L\log L+m\log L)\)

P5840 [COCI 2015] Divljak

先对 \(S_{1\sim n}\) 建出 AC 自动机,每次加入字符串,相当于在 AC 自动机上跑一遍,并给路径上节点打上一个标记(标记有种类,不同字符串种类不同)。查询即查询 \(S_x\) 末尾节点在 \(fail\) 树上子树内有多少不同标记。

可以线段树合并维护,但空间 \(O(L\log L)\) 被卡了,存在一种更加高效的方法,对于路径上节点 \(p_1,p_2,\dots,p_k\),我们对每个 \(p_i\) 执行从 \(p_i\) 到根的加操作,这样会导致重复计数,去重的方法是对于每个 \(\text{LCA}(p_i,p_{i+1})\),执行该点到根节点的路径减操作,这样就不会重复,具体方法不会证,但是是个很经典的 trick。时间复杂度 \(O(L\log L+m\log L)\)

UVA1502 GRE Words

sol

The End

貌似算法入门的时候很憧憬这个东西。

posted @ 2025-07-24 11:21  zuoqingyuan111  阅读(18)  评论(0)    收藏  举报