AC自动机
AC自动机总结
问题引入
在网上过滤一些不干净的内容,当用户输入的一段文字内容之后,通过字符串匹配算法查找用户输入的文本中是否存在敏感词。
因为网上流量大,用户多,如果用KMP算法来匹配,则需要对每个模式串进行逐一匹配,则可能需要,这样效率并不是很高。那么有没有更高效的方式呢?有,那就是AC自动机。
其是由1975年,于贝尔实验室由Alfred V. Aho 和 Margaret J.Corasick所提出,因此得名AC自动机。该算法至今在模式匹配领域呗广泛应用。
相对于KMP应用在单模式匹配,AC自动机的应用领域是多模式匹配。可以看成KMP是AC自动机退化成的情况。
AC自动机的简介
AC自动机是以Trie树为数据结构基础,以KMP的思想建立。
简单而言,建立AC自动机有两个步骤:
- 建立基础的Trie树结构:将所有的模式串构成一颗Trie树;
- 利用KMP的思想:对Trie树上的所有节点构造失配指针;
然后进行多模式匹配了。
AC自动机的核心思想还是寻找模式串中的内部规律,达到在每次失配时的高效跳转。
字典树构建
对于模式串的Trie树构建,其实就是将模式串的一次insert到Trie树中。其就是一个普通的Trie字典树,没啥特别的。这里需要明确一下Trie树中节点的含义:
Trie中的节点表示就是某个模式串的前缀,不妨称之为状态,一个节点就代表一个状态,Trie树中的边就是状态的转移。
形式化的说,对于若干个模式串,将它们构建成一颗字典树后的所有状态集合记为Q。
//建树模板
//假定都为小写字母集合;
class Trie{
private:
const int NODE_SIZE = 100000;
int trie[NODE_SIZE][26], cnt = 0;
bool end[NODE_SIZE];
public:
void insert(char* s, int len)
{
int p = 0;
for (int k = 0; k < len; k++) {
char ch = s[k] - 'a';
if (trie[p][ch] == 0) {
trie[p][ch] = ++cnt;
}
p = trie[p][ch];
}
end[p] = true; //标识该节点为某个字符串的尾部;
}
bool query(char* s, int len)
{
int p = 0;
for (int k = 0; k < len; k++) {
char ch = s[k] - 'a';
if (trie[p][ch] == 0) {
return false;
}
p = trie[p][ch];
}
return end[p] == true;
}
};
失配指针
在KMP中一旦失败,我们会立马转移到下一个节点进行匹配。类似的,AC自动机也有这样的机制,我们称之为失配指针。
AC自动机是借助fail指针来辅助进行多模式串的匹配。
状态u的fail指针指向另一个状态v,其中,且v是u的最长后缀(即在若干个后缀状态中取最长的一个作为fail指针)。
fail指针和KMP中的next指针的相似点和区别:
- 共同点:两者都是在失配时用于跳转的指针;
- 不同点:next指针求得是最长的前后缀,fail指针指向所有模式串的前缀中匹配当前状态的最长后缀;
构建失配指针
构建fail指针的基础思想:
考虑Trie树中当前节点u,u的父节点是p,p通过字符c的边指向u,即trie[p][c] = u。假设深度小于u的所有节点fail指针都已经求得.
- 如果trie[fail[p]][c]存在,则让u的fail指针指向trie[fail[p]][c]。相当于在p和fail[p]后面加了一个字符c,分别对应u和fail[u]。
- 如果trie[fail[p]][c]不存在,那么我们继续寻找trie[fail[fail[p]]][c]。重复1的步骤,直到fail指针指到根节点位置;
- 如果真没有了,就让fail指针指向根节点;
这样就完成fail指针的构建。如此看,的确是比KMP好理解。
样例
略,大家有需要可以在参考资料的OI_WIKI博客上看动图,这里我就不画蛇添足了。。。。。。
fail指针代码
因为需要保证小于当前节点u的深度的所有节点fail指针都已经遍历,这个可以通过BFS进行层次遍历。
代码相关解释:
- 队列q,用于维护深度层次关系,说明了就是层次遍历Trie树;
- fail[u]:节点u失配的转移的指针;
//非状态压缩版;
//queue<int> qu;
//状态压缩;
void buildFail()
{
//压入第一层的状态转移;
for (int i = 0; i < 26; i++) {
if (trie[0][i]) {
qu.push(trie[0][i]);
}
}
while (!qu.empty()) {
int u = qu.front();
qu.pop();
for (int i = 0; i < 26; i++) {
/*
如果trie[u][i]存在,那么需要将trie[u][i]的fail指针赋给trie[fail[u]][i],这里因为做了特殊处理所以不需要while循环递归;
*/
if (trie[u][i]) {
fail[trie[u][i]] = trie[fail[u]][i];
qu.push(trie[u][i]);
} else {
//否则令trie[u][i]指向trie[fail[u]][i]的状态;
trie[u][i] = trie[fail[u]][i];
}
}
}
return;
}
多模式匹配
有了trie树和fail指针数组后,我们就可以利用起来进行多模式匹配了:
int Query(char* text, int len)
{
//返回的匹配的答案;
int ans = 0;
int u = 0;
for (int i = 0; i < len; i++) {
//字典树上当前匹配到的节点;
u = text[i] - 'a';
//沿着u的fail指针往上跳,答案加上沿途遇到的终止状态的个数;为了避免重复统计,则需要给走过的位置打上标记;
for (int j = u; j && end[j] != -1; j = fail[j]) {
ans += end[j];
end[j] = -1;
}
}
return ans;
}
要是还没看明白,可以参考这篇Blog。
https://bestsort.cn/2019/04/28/402/
然后就是刷题了,先刷模板,在做灵活题。
参考资料
- https://zhuanlan.zhihu.com/p/80325757
- https://www.cnblogs.com/-Wallace-/p/14093790.html
- https://blog.csdn.net/weixin_40317006/article/details/81327188
- https://www.cnblogs.com/cmmdc/p/7337611.html
- https://www.cnblogs.com/DWVictor/p/10283013.html
- https://www.cnblogs.com/hyfhaha/p/10802604.html
- http://www.cppblog.com/mythit/archive/2009/04/21/80633.html
- https://oi-wiki.org/string/ac-automaton/
- https://www.cnblogs.com/wenzhixin/p/9448045.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】