算法学习笔记(20): AC自动机
AC自动机
前置知识:
字典树:可以参考我的另一篇文章 算法学习笔记(15): Trie(字典树)
KMP:可以参考 KMP - Ricky2007,但是不理解KMP算法并不会对这个算法的理解产生影响。
使用场景
AC自动机是一种著名的多模式匹配算法。
可以完成类似于KMP算法的工作,但是由单字符串的匹配变成了多字符串的匹配。
一般来说,会有很多子串,和一个母串。问题常是求字串在母串中的出现情况(包括位置,次数,等等)
算法思想与流程
我在Trie树一文中提到过这样一句话
而AC自动机的核心就在于通过对Trie树进行处理,使得在处理母串的信息时可以快速的进行状态转移。
可以类比KMP的算法流程,但是这不重要
例如子串有 aa
, ab
, abc
, b
。母串为 ababcba
。
由于我们是通过母串进行状态转移,所以需要先把所有字串的信息搞定
我们可以先处理子串,建一棵Trie树
明显,对于一个字串的匹配,是不可能在树上一路到底的,所以要构建匹配失败时的回退机制。也就是需要构建失配指针。
那么失配指针是干什么的?也就是用来在 Trie
树上向上跳,找到可以转移的一个节点,进行状态转移。
假如我现在在3号节点,并且我下一个需要转移的状态是 b
,很明显,我此时应该回退到1节点(其上第一个可以通过 b
转移的节点)并转移到4节点。如果再来一个 b
,也只能向上走到0号节点,然后转移到2号节点。
如此看来,我们完全可以暴力向上跳找到可转移的状态或者到达根为止。但是,这明显不够优秀,我们完全可以继承其子节点的。也就是继承 fail
的子节点。使得不需要暴力向上跳。
那说了半天,fail
到底指向啥?
假设父节点到当前节点转移的状态为 x
,父节点之上第一个可以通过 x
转移到下一个节点的节点为 u
,则 fail
指向 u
通过 x
转移过后的节点。
其实还有另一种解释的方法
fail
指向p
代表当前串的最长已知后缀。例如
aa
的最长已知后缀为a
,所以 3号节点的fail
指向 1号节点;abc
的最长已知后缀为空,所以5
号节点的fail
指向根节点。
好混乱,我尽力了……
那么核心代码……就是利用 BFS
来处理
void procFail(int * q) {
int head(0), tail(0);
for (int i(0); i < 26; ++i) {
if (kids[0][i]) q[tail++] = kids[0][i];
}
while (head ^ tail) {
int x = q[head++];
for (int i(0); i < 26; ++i) {
if (kids[x][i]) {
fail[kids[x][i]] = kids[fail[x]][i];
q[tail++] = kids[x][i];
} else kids[x][i] = kids[fail[x]][i];
}
} // procFail end
}
注意事项:一般来说,把 0
号作为根节点会比较方便。反正 0
上不可能有信息保存。
插入部分我就不需要讲了
匹配的判断
如何判断当前状态有没有匹配任何一个字串,只需要不断向上跳 fail
,看跳到的节点是不是代表着字串。
拿模板:【模板】AC 自动机(简单版) - 洛谷 为例。
插入的时候在最后标记一下有没有匹配:
void insert(string &s) {
int p(0);
for (int c : s) {
if (!kids[p][(c -= 'a')]) kids[p][c] = ++usage;
p = kids[p][c];
}
++cnt[p];
}
在匹配的时候暴力跳就是了:
int ACMatch(string & s) {
int p(0), ans(0);
for (int c : s) {
p = kids[p][(c -= 'a')];
for (int t(p); t && ~cnt[t]; t = fail[t]) {
ans += cnt[t], cnt[t] = -1;
}
}
return ans;
}
由于每一个串只能匹配一次,所以这里采用的清空的策略。并且标记清空,以免重复搜索。
失配树的应用
就拿模板题来说吧:【模板】AC 自动机(二次加强版) - 洛谷
他是要求所有字串的出现情况。
那么,我们先把每一个到达的状态计数。再通过 fail
指针向上跳求和。
但毕竟不能每一个节点都暴力跳,所以考虑在 fail
树上求和。
但是,我们不是有一个 q
来 BFS
吗?其中的 fail
是有序的:对于一个节点 x
,其 fail
一定在 x
之前被遍历到。
所以我们直接使用 q
即可。
那么合起来大概也就是这样:
inline void ACMatch(string &s) {
int p(0);
for (char c : s) {
p = kids[p][c - 'a'];
++cnt[p];
}
}
inline void ACCount(int * q) {
for (int i = usage; i; --i) {
cnt[fail[q[i]]] += cnt[q[i]];
}
}
但是每一个特定的字串出现的次数呢?
在插入时记住字串对应的节点,输出即可。
void insert(string &s, int i) {
int p(0);
for (int c : s) {
if (!kids[p][(c -= 'a')]) kids[p][c] = newNode();
p = kids[p][c];
}
pos[i] = p;
}
inline void ACOutput(int n) {
for (int i = 1; i <= n; ++i) {
cout << cnt[pos[i]] << '\n';
}
}
有这么一道题:
很明显,对于每一个位置,我们需要清理能匹配到的最长长度,所以我们需要预处理出最长长度:
inline void ACprepare(int * q) {
for (int i = 1; i <= usage; ++i) {
len[q[i]] = max(len[q[i]], len[fail[q[i]]]);
}
}
在清理时:
inline void ACclean(string &s) {
int p(0);
for (unsigned i(0), ie = s.size(); i < ie; ++i) {
p = kids[p][discrete(s[i])];
if (len[p]) for (unsigned j = i - len[p] + 1; j <= i; ++j)
s[j] = '*';
}
}
由于是引用的字符串,所以可以直接修改。
对状态的理解
在我们考试的时候有这么一道题:
这道题说难也难,说不难也不难。主要是看对于 AC自动机
状态转移的理解到不到位。
在匹配过程中,如果匹配到了出现的 w
,那么就要回到 len(w)
个状态前,继续匹配下一个字符。
很明显,需要用栈,并且由于需要一次弹出多个,所以最好用手写的栈。
核心代码如下:
string sub, pat;
cin >> sub >> pat;
insert(sub), procFail(Q);
int p = 0;
for (int i(0), ie = pat.size(); i < ie; ++i) {
p = kids[cps[ci]][pat[i] - 'a'];
cps[++ci] = p, ccs[ci] = pat[i];
if (match[p]) ci -= sub.size();
}
for (int i = 1; i <= ci; ++i) {
putchar(ccs[i]);
}
这里没有用到
fail
,那么为什么还要构建失配树?这是个好问题,因为,构建失配树的过程不仅仅构建了失配树,同时还令节点继承了其
fail
的子节点,所以需要构建的过程。
最后附上模板题【模板】AC 自动机(二次加强版) - 洛谷的代码:
#include <iostream>
#include <algorithm>
#include <string>
using namespace std;
const int N = 1e6 + 7;
int res[N], cnt[N], pos[N];
class ACAutomaton {
private:
int kids[N][26];
int fail[N], id[N], usage;
public:
ACAutomaton() : usage(0) {
}
inline int newNode() {
fill_n(kids[++usage], 26, 0);
cnt[usage] = fail[usage] = id[usage] = 0;
return usage;
}
void insert(string &s, int i) {
int p(0);
for (int c : s) {
if (!kids[p][(c -= 'a')]) kids[p][c] = newNode();
p = kids[p][c];
}
pos[i] = p;
}
void procFail(int * q) {
int head(0), tail(0);
for (int i(0); i < 26; ++i) {
if (kids[0][i])
fail[kids[0][i]] = 0, q[tail++] = kids[0][i];
}
while (head ^ tail) {
int x = q[head++];
for (int i(0); i < 26; ++i) {
if (kids[x][i]) {
fail[kids[x][i]] = kids[fail[x]][i];
q[tail++] = kids[x][i];
} else kids[x][i] = kids[fail[x]][i];
}
} // procFail end
}
void debug() {
for (int i = 0; i <= usage; ++i) {
printf("node %d (cnt %d) fail to %d:\n\t", i, cnt[i], fail[i]);
for (int j(0); j < 26; ++j) {
printf("%d ", kids[i][j]);
} puts("");
}
}
inline void ACMatch(string &s) {
int p(0);
for (char c : s) {
p = kids[p][c - 'a'];
++cnt[p];
}
}
inline void ACCount(int * q) {
for (int i = usage; i; --i) {
cnt[fail[q[i]]] += cnt[q[i]];
}
}
inline void ACOutput(int n) {
for (int i = 1; i <= n; ++i) {
cout << cnt[pos[i]] << '\n';
}
}
void clear() {
usage = -1;
newNode(); // clear 0
}
} ac;
int Q[N];
string s;
int main() {
cin.tie(0)->sync_with_stdio(false);
int n;
cin >> n;
for (int i = 1; i <= n; ++i) {
cin >> s;
ac.insert(s, i);
} ac.procFail(Q);
cin >> s;
ac.ACMatch(s);
ac.ACCount(Q);
ac.ACOutput(n);
return 0;
}
差不多了……下课
算法学习笔记(20): AC自动机的更多相关文章
- 学习笔记:AC自动机
话说AC自动机有什么用......我想要自动AC机 AC自动机简介: 首先简要介绍一下AC自动机:Aho-Corasick automation,该算法在1975年产生于贝尔实验室,是著名的多模匹配 ...
- 【学习笔记】ac自动机&fail树
定义 解决文本串和多个模式串匹配的问题: 本质是由多个模式串形成的一个字典树,由tie的意义知道:trie上的每一个节点都是一个模式串的前缀: 在trie上加入fail边,一个节点fail边指向这个节 ...
- SQL反模式学习笔记20 明文密码
目标:恢复或重置密码 反模式:使用明文存储密码 1.存储密码 使用明文存储密码或者在网络上传递密码是不安全的. 如果攻击者截取到你用来插入(或者修改)密码的sql语句,就可以获得密码. 黑客获 ...
- 某科学的PID算法学习笔记
最近,在某社团的要求下,自学了PID算法.学完后,深切地感受到PID算法之强大.PID算法应用广泛,比如加热器.平衡车.无人机等等,是自动控制理论中比较容易理解但十分重要的算法. 下面是博主学习过程中 ...
- Miller-Rabin 与 Pollard-Rho 算法学习笔记
前言 Miller-Rabin 算法用于判断一个数 \(p\) 是否是质数,若选定 \(w\) 个数进行判断,那么正确率约是 \(1-\frac{1}{4^w}\) ,时间复杂度为 \(O(\log ...
- Ext.Net学习笔记20:Ext.Net FormPanel 复杂用法
Ext.Net学习笔记20:Ext.Net FormPanel 复杂用法 在上一篇笔记中我们介绍了Ext.Net的简单用法,并创建了一个简单的登录表单.今天我们将看一下如何更好是使用FormPanel ...
- C / C++算法学习笔记(8)-SHELL排序
原始地址:C / C++算法学习笔记(8)-SHELL排序 基本思想 先取一个小于n的整数d1作为第一个增量(gap),把文件的全部记录分成d1个组.所有距离为dl的倍数的记录放在同一个组中.先在各组 ...
- Manacher算法学习笔记 | LeetCode#5
Manacher算法学习笔记 DECLARATION 引用来源:https://www.cnblogs.com/grandyang/p/4475985.html CONTENT 用途:寻找一个字符串的 ...
- golang学习笔记20 一道考察对并发多协程操作一个共享变量的面试题
golang学习笔记20 一道考察对并发多协程操作一个共享变量的面试题 下面这个程序运行的能num结果是什么? package main import ( "fmt" " ...
- Johnson算法学习笔记
\(Johnson\)算法学习笔记. 在最短路的学习中,我们曾学习了三种最短路的算法,\(Bellman-Ford\)算法及其队列优化\(SPFA\)算法,\(Dijkstra\)算法.这些算法可以快 ...
随机推荐
- JAVA pta 前三次大作业回顾与分析
一.前言:总结三次题目集的知识点.题量.难度等情况 今年初次接触java,通过这三次大作业的练习,我对java有了一定的认识,相比于其他编程语言来说,java更复杂,要求也更严谨,需要掌握的知识也更多 ...
- jetson nano 4gb记录
常用命令 Ctrl alt t 打开终端gnome-session-properties 打开开机自启动管理界面 ifconfig 查看ip xrandr --fb 1200x800 调节分辨率 ll ...
- Call API URL连接拼参数的方法
void TestNTFD() { try { try { ...
- 安装 TensorFlow 参考过的资料
anaconda 的新环境创建 https://blog.csdn.net/yandajiangjun/article/details/102615912
- 单调栈应用--视野总和 go版本
1.视野总和描叙:有n个人站队,所有的人全部向右看,个子高的可以看到个子低的发型,给出每个人的身高,问所有人能看到其他人发现总和是多少.输入:4 3 7 1输出:2解释:个子为4的可以看到个子为3的发 ...
- 入门文章学习(一)-Beginner Tutorial
Abstract: 参照"背景知识查阅"一文的学习路径,对几篇文章的学习做了记录.这是"Beginner Tutorial"一文的学习笔记. 文章链接: htt ...
- CMake指定的任务可执行文件"cmd.exe" 未能运行。System.IO.IOException:未能创建临时文件。临时文件夹已满或其路径不正确。对路径"......exec.cmd "的访问被拒绝
我觉得是我使用VS2022的原因,网上也没有找到相同的问题.
- GO语言http请求方法,可以携带请求头Header与cookie
1.目录 2.main.go package main import "fmt" import "demo/common/http" func main() { ...
- org.xml.sax.SAXNotRecognizedException: SAX feature 'http://apache.org/xml/features/allow-java-encodings' not recognized.
tomcat启动服务后,解析xml等文件会报错org.xml.sax.SAXNotRecognizedException: SAX feature 'http://apache.org/xml/fea ...
- Oracle 取Group By 第一条
select *from (select emp.*,row_number() over(partition by deptno order by rownum) cn from emp)where ...