当前位置: 首页 > news >正文

AC自动机 学习笔记

又一神秘字符串匹配算法。

前言

昨天晚上LZY发现自己的对拍有锅,急忙去修,最终成功将其倒退了一个版本。

介绍

AC 自动机是一种多模式串匹配算法,由 Alfred V. Aho 和 Margaret J.Corasick 发明,所以AC自动机的全名是:

\[\Large \mathcal{Aho–Corasick\space Automaton} \]

有A有C但没有AC

AC自动机的主要作用便是让多个模式串去匹配一个文本串
听起来有点像KMP?
确实用到了KMP的思想,此外也用到的Trie树,所以AC自动机是Trie树上的自动机
所以不会KMP和Trie树的请掉头。
下面我们详细说明AC自动机的算法过程。

过程

回忆KMP

我们首先回忆一下你kmp是如何匹配的。
假设文本串为abacabababc,模式串为abab,假设预处理已完成,现在进行匹配。

首先我们很顺利的匹配了三个字符:

abacabababc
^^^!
abab

发现下一个字符匹配不上,KMP没有放弃所有已匹配的结果,既然已经匹配上了aba为什么不让a继续进行匹配呢?

abacabababc^!ab

发现从a开始匹配还是匹配不上,于是只能从头开始了。

abacabababc!a

匹配不上,跳过!

abacabababc^a
abacabababc^^^^abab

成功匹上一个模式串!但模式串后面没有了,所以我们让模式串的公共前后缀ab继续匹配。

abacabababc^^ab
abacabababc^^^^abab

又成功匹配上一个模式串!后面就没有了,匹配完成。

可以发现KMP的思想就是能不省就不省,最大化利用已经匹配好的串继续向下匹配。

AC自动机上的匹配

AC自动机上的匹配与KMP的匹配大同小异,唯一不同的地方就是失配后的决策。

  • KMP在失配后会将当前最长公共前后缀移上来继续尝试匹配。
  • AC自动机在失配后会尝试通过去除已经匹配的部分的一段前缀继续尝试匹配。

失配决策不一样的原因就在于AC自动机处理的是多模式串匹配,在其中一个没匹配上的时候,AC自动机会尝试匹配别的串。
这样说好像有点不太对,举个例子就明白了:
有五个模式串:hershesheighterhterthought,与一个文本串:shersheighthoughter

shersheighthoughter
^^^
she
sheighter

首先shesheighter都匹配上了sheshe完成了匹配,但sheighter将在下一步失配。
于是考虑去除shes前缀。

hersheighthoughter
^^
her

取出后sheightershe由于没有开头的s无法继续匹配,但her可以继续尝试匹配。

hersheighthoughter
^^^
her

her匹配成功,由于无论尝试去除任何her的前缀对无法尝试新的匹配,所以直接去除整串整串也是前缀的一种

sheighthoughter
^^^
she
sheighter

she再次完成匹配,由于sheighter还可以继续尝试匹配,所以我们先不去除前缀。

sheighthoughter
^^^^^^^!
sheighter

sheighter无法继续匹配,尝试去除前缀sheig

hthoughter
^^
hter

去除后hter尝试匹配。

hthoughter
^^!
hter

匹配失败尝试去除前缀h

thoughter
^^
thought

去除后thought,尝试匹配。

thoughter
^^^^^^^
thought

匹配成功,尝试去除前缀thoug继续匹配。

hter
^^
hter

去除后hter尝试匹配。

hter
^^^^
hter

匹配成功,算法结束。
以上就是AC自动机多模式串匹配的步骤,先别说你会不会写,但你肯定大概理解了。

Trie树优化匹配

上面那一堆找了一堆前缀,找前缀这活得找“专业人士”Trie树来干。
我们将上面一堆模式串扔进一颗Trie树里,就会得到这样的东西:
字典树
怎么画出来这么一个玩意
我习惯将字符标到节点上。
我们都知道,Trie树上的每一个点都表示一个字符串,那么我们完全可以整一个失配指针(\(fail\))指向每一个节点失配后去除前缀后的后缀部分
就拿上面这一步举例:

sheighthoughter
^^^^^^^!
sheighter

sheighter无法继续匹配,尝试去除前缀sheig
我们完全可以将sheight节点的 \(fail\) 指向ht
失配
这样就可以快速找到下一个要匹配的串。

但说这么多,这个失配指针该这么指呢?
在Trie树中,每一个节点都有一堆指针,表示下一个字符,像sheigh这个节点,只有一个指向下一个节点sheigh的指针,为什么我们不尝试将sheigh指针在指向heht呢?
假设sheigh已经指向了heht,那么在找sheight的失配指针的时候,直接找它父亲所指向t的节点就找到了。
为什么可以让sheigh指向heht呢?
观察一下如果将sheigh的前缀扔掉,剩下h那么h这个字符串在往后接就可以接成heht也就是说sheigh的后缀与heht的前缀有公共部分,这样如果sheight这个t失配了,sheigh可以通过去除前缀到达ht这个串。
具体过程请看代码实现:

struct node{int son[26];                    //子节点指针int fail;                       //失配指针void init(){                    //初始化memset(son,0,sizeof(son));ans=fail=id=0;}
}T[NUM];
void insert(string s){              //插入一个节点,Trie树操作int u=0;for(char i:s){int &son=T[u].son[i-'a'];if(!son) son=++tot,T[son].init();u=son;}
}
void build(){                        //构建fail指针queue<int> q;                    for(int i=0;i<26;++i){if(T[0].son[i]) q.push(T[0].son[i]);  //先将跟的子节点入队}while(!q.empty()){int u=q.front();             //取出队首q.pop();for(int i=0;i<26;++i){if(T[u].son[i]){         //如果有这个子节点T[T[u].son[i]].fail=T[T[u].fail].son[i];  //子节点的fail就指向当前节点fail的对应节点。q.push(T[u].son[i]);}else{T[u].son[i]=T[T[u].fail].son[i];  //将子节点指向fail的对应子节点}}}
}

另附一张来自oi-wiki的图:

可以手摸一下理解其过程。

统计答案

先附上一张来自oi-wiki的图:

可以结合前面说过的AC自动机上的匹配理解其过程。
我们总不能在这些节点间跳来跳去把,这也太耗时间了。
我们可以发现,若只保留fail指针,那么剩余的图就是一颗树。
这是很显然的,所有节点的fail都指向比自身深度浅的节点,总共有节点数减一个加点,就是一棵树。
这样AC自动机上的问题就可以转移成子树求和的问题。

【模板】AC 自动机\(^{luoguP5357}\)

三倍经验题
子树求和问题可以使用拓扑排序或者树上dp(dfs),拓扑排序需要维护一下每个节点的度数。
下面使用拓扑排序过这道题

#include<bits/stdc++.h>
using namespace std;
const int NUM=2e5+10;
const int NUMM=2e6+10;int n;
namespace AC{struct node{int son[26];int ans;int fail;int deg;int id;void init(){memset(son,0,sizeof(son));ans=fail=id=0;}}T[NUM];int tot;int ans[NUM],pid;void init(){tot=pid=0;T[0].init();}void insert(string s,int &idx){int u=0;for(char i:s){int &son=T[u].son[i-'a'];if(!son) son=++tot,T[son].init();u=son;}if(!T[u].id) T[u].id=++pid;idx=T[u].id;}void build(){queue<int> q;for(int i=0;i<26;++i){if(T[0].son[i]) q.push(T[0].son[i]);}while(!q.empty()){int u=q.front();q.pop();for(int i=0;i<26;++i){if(T[u].son[i]){T[T[u].son[i]].fail=T[T[u].fail].son[i];T[T[T[u].fail].son[i]].deg++; //记录度数q.push(T[u].son[i]);}else{T[u].son[i]=T[T[u].fail].son[i];}}}}void query(string t){int u=0;for(char i:t){u=T[u].son[i-'a'];T[u].ans++;}}void topu(){                      //拓扑排序queue<int> q;for(int i=0;i<=tot;++i){if(T[i].deg==0) q.push(i);}while(!q.empty()){int u=q.front();q.pop();ans[T[u].id]=T[u].ans;int v=T[u].fail;T[v].ans+=T[u].ans;if(!--T[v].deg) q.push(v);} }
}
using namespace AC;string s;
int idx[NUM];int main(){init();cin>>n;for(int i=1;i<=n;++i){cin>>s;insert(s,idx[i]);ans[i]=0;}build();cin>>s;query(s);topu();for(int i=1;i<=n;++i){cout<<ans[idx[i]]<<'\n';}return 0;
}

不写DFS的原因:

  1. 我能力不足或是说我懒不想花时间去找根。
  2. 这篇其实是我贺的oi-wiki上的。

后记

AC自动机其实并不是很难,但出题人总是出一些奇奇怪怪的题。
反正就是不能让你好受。

http://www.jsqmd.com/news/1289801/

相关文章:

  • 拯救消失的网页记忆:Wayback Machine浏览器扩展完全指南
  • 为什么需要人在回路?达尔文.skill独特的三层守关机制详解
  • 如何快速部署MetaTube插件:Jellyfin智能元数据刮削终极指南
  • 芯聆CLD6255(4 x 190W, 2 x 380W 或 2.1 模式 (2x190W + 1x380W ) @1% THD 数字输入 D 类音频播放器)
  • 命令行生成BIN文件并修改内容后与固件合并烧录至芯片
  • 软件测试工程师到底是做什么的?一文讲清职责、技能与成长路径
  • 揭秘XStreaming背后的WebRTC技术:打造稳定流畅的串流体验
  • 为什么说地震后的机房评估,必须把“深度除尘检测”作为前置必选项?
  • Apicurio Registry审计日志:跟踪Schema变更历史的终极指南
  • 中小企业财税数字化怎么选?业财税一体化软件推荐与亿企赢深度解析 - 速递信息
  • 【AI生成产品展示图终极指南】:20年电商视觉专家亲授,3步搞定高转化率AI图,错过再等半年!
  • KMS智能激活工具完整指南:一键永久激活Windows和Office的免费解决方案
  • Jenkins备份与恢复策略:确保你的构建历史永不丢失
  • 电销机器人为什么越来越多的初创团队选择蓝鲸智呼? - 生活动态圈
  • 从安装到实战:EventBus完整使用教程(含代码示例)
  • Home Assistant界面优化必装插件:fold-entity-row安装与使用指南
  • 开发者视角:Ninjabrain-Bot的末地城生成机制模拟与代码架构
  • 2026医疗器械零售企业品牌曝光提升指南:具备合规内容构建能力的SEO/GEO服务商大盘点 合作避坑全解析 - U渠道
  • ChatGPT语音模式:让碎片时间高效交互,比打字更直观实用!
  • 国家中小学智慧教育平台电子课本下载:3分钟搞定所有教材PDF
  • Apicurio Registry与OpenShift部署:容器平台最佳实践
  • 什么叫做强校垄断?
  • 2026广西公考机构多维度横向测评:师资、教研、智能系统谁更胜一筹? - 天涯视角
  • 2026年财税合规软件推荐:亿企赢凭什么让小微企业和代账公司都选它? - 速递信息
  • AI环境监测合规生死线:GDPR+《生态环境监测条例》双框架下数据采集、存储与模型审计强制要求(仅限首批内测机构获取)
  • 如何在Android设备上高效运行Windows应用:Winlator完整指南
  • Kritis:Kubernetes应用部署时的终极策略执行者,如何保障容器安全?
  • 揭秘fold-entity-row:让Home Assistant实体卡片折叠更简单的终极解决方案
  • 5个终极策略:用virtio-win彻底提升Windows虚拟化性能
  • Kritis核心功能解析:ImageSecurityPolicy如何拦截不安全容器部署