「学习笔记」AC 自动机

点击查看目录

前置:「学习笔记」字符串基础:Hash,KMP与Trie

好像对例题的讲解越来越抽象了?

算法

问题

求 \(n\) 个单词在一个长度为 \(m\) 的文章里出现过多少个。

思路

很多文章都说这玩意是 Trie 树 + KMP,我觉得确实可以这样理解但是不完全一样。

KMP 有两种理解方式:求 Border 或失配指针,AC 自动机用的是「失配指针」这个理解方式。

KMP 的失配指针指向的是一个最长的与后缀一样的前缀,这样仍然可以继续匹配,而且使需要重新匹配的地方尽量短。

AC 自动机 \(\text{fail}\) 指针指向的则是一个存在于这个 Trie 树中的最长的与真后缀相同的字符串

依旧是拿 OI-wiki 的图举个例子:

比如单词 she,它的真后缀有 hee (\(\leftarrow\) 这个真后缀是空的),其中 he 存在于 Trie 树中,则让 \(9\) 号节点的 \(\text{fail}\) 指针指向最长的 he 的末尾节点 \(2\) 号节点。

再如单词 her,它的真后缀有 err ,但是只有 存在于 Trie 树中,则让 \(3\) 号节点的 \(\text{fail}\) 指针指向根节点 \(0\)。

那么怎么找到 \(\text{fail}\) 指针呢?

我们设当前节点 \(p\) 代表的字符是 \(c\),则 \(p\) 的 \(\text{fail}\) 指针应指向 \(p\) 的父亲的 \(\text{fail}\) 指针的代表 \(c\) 的儿子。

例如上图中,\(9\) 代表的字符是 e,\(9\) 的父亲是 \(8\),\(8\) 的 \(\text{fail}\) 指针指向 \(1\),\(1\) 的代表 e 的儿子是 \(2\),因此 \(9\) 的 \(\text{fail}\) 指针指向 \(2\) 号节点。

很好理解吧!xrlong said:没看出来。

但是有个问题,比如图中的六号节点应指向哪里?\(6\) 的父亲 \(5\) 的 \(\text{fail}\) 指针 \(10\) 的代表 s 的儿子不存在,但是很明显应指向 \(7\) 啊!

那就跳到 \(10\) 号节点的 \(\text{fail}\) 指针 \(0\),找 \(0\) 的代表 s 的儿子 \(7\)。但是每次跳很多 \(\text{fail}\) 指针效率太低了,怎么办?

那就魔改一下这棵树!如果 \(p\) 不存在代表 \(c\) 的儿子,那就让 \(p\) 代表 \(c\) 的儿子指向 \(p\) 的 \(\text{fail}\) 指针的代表 \(c\) 的儿子。

就像下面这幅图:

最后再次放一下 OI-wiki 上的完整动图:

  1. 蓝色结点:BFS 遍历到的结点 \(u\)。
  2. 蓝色的边:当前结点下,AC 自动机修改字典树结构连出的边。
  3. 黑色的边:AC 自动机修改字典树结构连出的边。
  4. 红色的边:当前结点求出的 \(\text{fail}\) 指针。
  5. 黄色的边:\(\text{fail}\) 指针。
  6. 灰色的边:字典树的边。

代码

namespace ACAUTOMATON {
class ACAutomaton {
private:
ll cnt = 0, nxt[N][26], fail[N], end[N];
public:
inline void Clear () {
cnt = 0;
memset (nxt, 0, sizeof (nxt));
memset (end, 0, sizeof (end));
memset (fail, 0, sizeof (fail));
return;
}
inline void Insert (char* s) {
ll p = 0, len = strlen (s + 1);
_for (i, 1, len) {
ll c = s[i] - 'a';
if (!nxt[p][c]) nxt[p][c] = ++cnt;
p = nxt[p][c];
}
++end[p];
return;
}
inline void Build () {
std::queue <ll> q;
_for (i, 0, 25) if (nxt[0][i]) fail[nxt[0][i]] = 0, q.push (nxt[0][i]);
while (!q.empty ()) {
ll u = q.front (); q.pop ();
_for (i, 0, 25) {
if (nxt[u][i]) fail[nxt[u][i]] = nxt[fail[u]][i], q.push (nxt[u][i]);
else nxt[u][i] = nxt[fail[u]][i];
}
}
return;
}
inline ll Query (char* s) {
ll now = 0, len = strlen (s + 1), ans = 0;
_for (i, 1, len) {
now = nxt[now][s[i] - 'a'];
for (ll p = now; p && ~end[p]; p = fail[p]) ans += end[p], end[p] = -1;
}
return ans;
}
};
}

例题

Keywords Search

板子题。

玄武密码

在每个单词结尾的节点往前跑,看哪个节点深度最高且被访问过。

单词

记录每个点被访问过多少次,但直接记录时间会爆炸。

可以考虑延迟下传访问次数。

病毒

在 trie 树上找一个包括根节点的环,能找到的话直接顺着这个环不断跑就可以构造出无限长的安全代码。

最短母串

用哈希可以随便杀啊!但是这是 AC 自动机题单,所以我要用 AC 自动机写 DP(悲

\(f_{u, sta}\) 表示到节点 \(u\) 时,已经经过的字符串状态为 \(sta\) 时的最短字符串。

然后不难发现直接暴力广搜转移即可。

文本生成器

\(f_{u, l, b}\) 表示到节点 \(u\) 时,已经经过 \(l\) 个字符,「是否已经出现过给定串」的答案为 \(b(b\in\{0, 1\})\) 时的可读文本数量。

直接暴力广搜转移即可。

背单词

首先建出整个 AC 自动机,然后查询每个字符串的答案。

查询的过程有点说不太清,直接看码罢。

注意每次查询时把经过的节点标记一下,只能从标记过的节点转移。

为啥要用线段树啊。

貌似没人有我这个方法?那贴一份代码:

点击查看代码
const ll N = 3e5 + 10;

namespace ACAUTOMATON {
class ACAutomaton {
public:
ll cnt = 0, nxt[N][26], jl[N], fail[N], f[N];
public:
inline void Clear () {
_for (i, 0, cnt) {
memset (nxt[i], 0, sizeof (nxt[i]));
fail[i] = f[i] = jl[i] = 0;
}
cnt = 0;
return;
}
inline void Insert (std::string s) {
ll p = 0, len = s.length () - 1;
_for (i, 0, len) {
ll c = s[i] - 'a';
if (!nxt[p][c]) nxt[p][c] = ++cnt;
p = nxt[p][c];
}
return;
}
inline void Build () {
std::queue <ll> q;
_for (i, 0, 25) if (nxt[0][i]) fail[nxt[0][i]] = 0, q.push (nxt[0][i]);
while (!q.empty ()) {
ll u = q.front (); q.pop ();
_for (i, 0, 25) {
if (nxt[u][i]) fail[nxt[u][i]] = nxt[fail[u]][i], q.push (nxt[u][i]);
else nxt[u][i] = nxt[fail[u]][i];
}
}
return;
}
inline ll GetAns (std::string s, ll w) {
ll p = 0, len = s.length () - 1, num = 0;
_for (i, 0, len) {
ll c = s[i] - 'a';
jl[nxt[p][c]] = 1;
if (jl[fail[nxt[p][c]]]) f[nxt[p][c]] = std::max (f[nxt[p][c]], f[fail[nxt[p][c]]]);
num = std::max (num, f[nxt[p][c]]);
p = nxt[p][c];
}
return f[p] = std::max (f[p], num + w);
}
};
} namespace SOLVE {
ll n, m, w[N], ans; std::string s[N];
ACAUTOMATON::ACAutomaton ac;
inline ll rnt () {
ll x = 0, w = 1; char c = getchar ();
while (!isdigit (c)) { if (c == '-') w = -1; c = getchar (); }
while (isdigit (c)) x = (x << 3) + (x << 1) + (c ^ 48), c = getchar ();
return x * w;
}
inline void In () {
ac.Clear ();
n = rnt (), ans = 0;
_for (i, 1, n) {
std::cin >> s[i], w[i] = rnt ();
if (w < 0) continue;
ac.Insert (s[i]);
}
return;
}
inline void Solve () {
ac.Build ();
_for (i, 1, n) {
if (w[i] < 0) continue;
ans = std::max (ans, ac.GetAns (s[i], w[i]));
}
return;
}
inline void Out () {
printf ("%lld\n", ans);
return;
}
}

密码

首先如果存在一个随意填的位置,那么方案数至少为 \(52>42\)。例如:

7 2
good
day

*goodaygooday** 的位置可以填 \(26\) 个字母,方案数至少为 \(2\times26=52\)。

那么只要不存在随意填的位置,输出就比较方便了。

设 \(f_{u, l, sta}\) 表示到节点 \(u\),字符串长度为 \(l\),已经经过的字符串状态为 \(sta\) 时的最短字符串,直接暴力广搜转移算出方案数,如果小于 \(42\) 就爆搜每种方案即可。

代码比较恶心,贴一下:

点击查看代码
namespace ACAUTOMATON {
class ACAutomaton {
private:
ll cnt = 0, tot = 1, nxt[N][26], fail[N], end[N], f[N][30][M], jl[N][30][M][2];
class APJifengc { public: ll u, l, s; };
std::pair <ll, ll> vis[30 * 45];
std::vector <ll> answer;
char temp[N];
public:
inline void Insert (char *s, ll id) {
ll p = 0, len = strlen (s + 1);
_for (i, 1, len) {
ll c = s[i] - 'a';
if (!nxt[p][c]) nxt[p][c] = ++cnt;
p = nxt[p][c];
}
end[p] |= 1 << (id - 1);
return;
}
inline void Build () {
std::queue <ll> q;
_for (i, 0, 25) if (nxt[0][i]) fail[nxt[0][i]] = 0, q.push (nxt[0][i]);
while (!q.empty ()) {
ll u = q.front (); q.pop ();
_for (i, 0, 25) {
if (nxt[u][i]) fail[nxt[u][i]] = nxt[fail[u]][i], end[nxt[u][i]] |= end[nxt[fail[u]][i]], q.push (nxt[u][i]);
else nxt[u][i] = nxt[fail[u]][i];
}
}
return;
}
inline ll BFS (ll target,ll m) {
std::queue <APJifengc> q;
ll ans = 0; f[0][0][0] = 1;
q.push ((APJifengc){0, 0, 0});
while (!q.empty ()) {
ll u = q.front ().u, l = q.front ().l, s = q.front ().s; q.pop ();
if (l > m) break;
if (s == target && l == m) ans += f[u][l][s];
_for (i, 0, 25) {
ll v = nxt[u][i], ln = l + 1, st = s | end[v];
if (!f[v][ln][st]) q.push ((APJifengc){v, ln, st});
f[v][ln][st] += f[u][l][s];
}
}
return ans;
}
inline ll DFS (ll u, ll l, ll s, ll target, ll m) {
if (jl[u][l][s][0]) return jl[u][l][s][1];
jl[u][l][s][0] = 1;
if (l == m) return jl[u][l][s][1] = (s == target);
_for (i, 0, 25) jl[u][l][s][1] |= DFS (nxt[u][i], l + 1, s | end[nxt[u][i]], target, m);
return jl[u][l][s][1];
}
inline void PrintAns (ll u, ll l, ll s, ll m) {
if (!jl[u][l][s][1]) return;
if (l == m) { puts (temp + 1); return; }
_for (i, 0, 25) temp[l + 1] = i + 'a', PrintAns (nxt[u][i], l + 1, s | end[nxt[u][i]], m);
return;
}
};
} namespace SOLVE {
ll n, m, ans; char s[20];
ACAUTOMATON::ACAutomaton ac;
inline ll rnt () {
ll x = 0, w = 1; char c = getchar ();
while (!isdigit (c)) { if (c == '-') w = -1; c = getchar (); }
while (isdigit (c)) x = (x << 3) + (x << 1) + (c ^ 48), c = getchar ();
return x * w;
}
inline void In () {
m = rnt (), n = rnt ();
_for (i, 1, n) {
scanf ("%s", s + 1);
ac.Insert (s, i);
}
return;
}
inline void Solve () {
ac.Build ();
ans = ac.BFS ((1 << n) - 1, m);
if (ans <= 42) ac.DFS (0, 0, 0, (1 << n) - 1, m);
return;
}
inline void Out () {
printf ("%lld\n", ans);
if (ans <= 42) ac.PrintAns (0, 0, 0, m);
return;
}
}

禁忌

有点像 GT 考试

设 \(f_{i, u}\) 表示长度为 \(i\),到了节点 \(u\) 的串的期望伤害。

\[f_{i, u} = \frac{1}{alphabet}\sum_{son_{v,c} = u} f_{i - 1,v}
\]

但是 \(len\le10^9\),不能直接转移。

于是套一下矩阵乘法就好了。

码:

点击查看代码
namespace MATRIX {
class Matrix {
private:
ll n; ldb a[N][N];
public:
inline ldb* operator [] (ll x) { return a[x]; }
inline void Init (ll nn) { n = nn, memset (a, 0, sizeof (a)); return; }
inline Matrix operator * (Matrix another) const {
Matrix ans; ans.Init (n);
_for (i, 0, n) _for (j, 0, n) _for (k, 0, n)
ans[i][j] += a[i][k] * another[k][j];
return ans;
}
inline void Print () {
printf ("%lld\n", n);
_for (i, 0, n) { _for (j, 0, n) printf ("%Lf ", a[i][j]); puts (""); }
puts ("");
return;
}
};
} namespace ACAUTOMATON {
class ACAutomaton {
private:
ll cnt = 0, nxt[N][26], fail[N], end[N];
public:
inline void Insert (std::string s) {
ll p = 0, len = s.length () - 1;
_for (i, 0, len) {
ll c = s[i] - 'a';
if (!nxt[p][c]) nxt[p][c] = ++cnt;
p = nxt[p][c];
}
end[p] = 1;
return;
}
inline ll Build (ll alphabet) {
std::queue <ll> q;
_for (i, 0, alphabet - 1) if (nxt[0][i]) fail[nxt[0][i]] = 0, q.push (nxt[0][i]);
while (!q.empty ()) {
ll u = q.front (); q.pop ();
_for (i, 0, alphabet - 1) {
if (nxt[u][i]) fail[nxt[u][i]] = nxt[fail[u]][i], q.push (nxt[u][i]);
else nxt[u][i] = nxt[fail[u]][i];
}
end[u] |= end[fail[u]];
}
return cnt;
}
inline MATRIX::Matrix GetMatrix (ll alphabet) {
MATRIX::Matrix ma; ma.Init (cnt + 1);
_for (i, 0, cnt) {
_for (j, 0, alphabet - 1) {
if (end[nxt[i][j]]) ma[i][0] += 1.0 / (ldb)(alphabet), ma[i][cnt + 1] += 1.0 / (ldb)(alphabet);
else ma[i][nxt[i][j]] += 1.0 / (ldb)(alphabet);
}
}
ma[cnt + 1][cnt + 1] = 1.0;
return ma;
}
};
} namespace SOLVE {
ll n, m, len, alphabet;
std::string s[N];
MATRIX::Matrix ans;
ACAUTOMATON::ACAutomaton ac;
inline ll rnt () {
ll x = 0, w = 1; char c = getchar ();
while (!isdigit (c)) { if (c == '-') w = -1; c = getchar (); }
while (isdigit (c)) x = (x << 3) + (x << 1) + (c ^ 48), c = getchar ();
return x * w;
} inline MATRIX::Matrix FastPow (MATRIX::Matrix a, ll b) {
MATRIX::Matrix an; an.Init (m);
_for (i, 0, m) an[i][i] = 1.0;
while (b) {
if (b & 1) an = an * a;
a = a * a, b >>= 1;
}
return an;
} inline void In () {
n = rnt (), len = rnt (), alphabet = rnt ();
_for (i, 1, n) {
std::cin >> s[i];
ac.Insert (s[i]);
}
return;
}
inline void Solve () {
m = ac.Build (alphabet) + 1;
MATRIX::Matrix ma = ac.GetMatrix (alphabet);
ans.Init (m), ans[0][0] = 1.0;
ma = FastPow (ma, len), ans = ans * ma;
return;
}
inline void Out () {
printf ("%.10Lf\n", ans[0][m]);
return;
}
}
\[\Huge{\mathfrak{The\ End}}
\]

「学习笔记」AC 自动机的更多相关文章

  1. 「学习笔记」Min25筛

    「学习笔记」Min25筛 前言 周指导今天模拟赛五分钟秒第一题,十分钟说第二题是 \(\text{Min25}​\) 筛板子题,要不是第三题出题人数据范围给错了,周指导十五分钟就 \(\text{AK ...

  2. 「学习笔记」FFT 之优化——NTT

    目录 「学习笔记」FFT 之优化--NTT 前言 引入 快速数论变换--NTT 一些引申问题及解决方法 三模数 NTT 拆系数 FFT (MTT) 「学习笔记」FFT 之优化--NTT 前言 \(NT ...

  3. 「学习笔记」FFT 快速傅里叶变换

    目录 「学习笔记」FFT 快速傅里叶变换 啥是 FFT 呀?它可以干什么? 必备芝士 点值表示 复数 傅立叶正变换 傅里叶逆变换 FFT 的代码实现 还会有的 NTT 和三模数 NTT... 「学习笔 ...

  4. 「学习笔记」Treap

    「学习笔记」Treap 前言 什么是 Treap ? 二叉搜索树 (Binary Search Tree/Binary Sort Tree/BST) 基础定义 查找元素 插入元素 删除元素 查找后继 ...

  5. 「学习笔记」字符串基础:Hash,KMP与Trie

    「学习笔记」字符串基础:Hash,KMP与Trie 点击查看目录 目录 「学习笔记」字符串基础:Hash,KMP与Trie Hash 算法 代码 KMP 算法 前置知识:\(\text{Border} ...

  6. 「学习笔记」平衡树基础:Splay 和 Treap

    「学习笔记」平衡树基础:Splay 和 Treap 点击查看目录 目录 「学习笔记」平衡树基础:Splay 和 Treap 知识点 平衡树概述 Splay 旋转操作 Splay 操作 插入 \(x\) ...

  7. 「笔记」AC 自动机

    目录 写在前面 定义 引入 构造 暴力 字典图优化 匹配 在线 离线 复杂度 完整代码 例题 P3796 [模板]AC 自动机(加强版) P3808 [模板]AC 自动机(简单版) 「JSOI2007 ...

  8. 「刷题笔记」AC自动机

    自动AC机 Keywords Research 板子题,同luoguP3808,不过是多测. 然后多测不清空,\(MLE\)两行泪. 板子放一下 #include<bits/stdc++.h&g ...

  9. 「学习笔记」wqs二分/dp凸优化

    [学习笔记]wqs二分/DP凸优化 从一个经典问题谈起: 有一个长度为 \(n\) 的序列 \(a\),要求找出恰好 \(k\) 个不相交的连续子序列,使得这 \(k\) 个序列的和最大 \(1 \l ...

  10. 算法学习笔记(20): AC自动机

    AC自动机 前置知识: 字典树:可以参考我的另一篇文章 算法学习笔记(15): Trie(字典树) KMP:可以参考 KMP - Ricky2007,但是不理解KMP算法并不会对这个算法的理解产生影响 ...

随机推荐

  1. 介绍String、StringBuffer和StringBuilder

    1. String类:  位于java.lang包,早期版本JDK1.0,继承Object类,实现java.io.Serializable, Comparable<String>, Cha ...

  2. 替代if esle 的高级方法

    if else 是入门最常遇到的一种结构,这种结构简单易懂,深受初学者喜爱.但是 If-Else通常是一个糟糕的选择. 它的可读性差,如果用的太多,会导致结构重构困难.今天我就介绍替代 If-Else ...

  3. ubuntu14.04 cm12.0 genymotion

    前几天在ubuntu14.04编译过android5.0.1源码,但是呢?不知怎么运行不起来,后来又试了试把img文件在win平台运行虚拟机不知怎么还是不行,再后来想通过刷机运行学习framework ...

  4. Winform 使用DotNetBar 设置界面为Office2007 样式

    public partial class FrmMain : DevComponents.DotNetBar.Office2007RibbonForm { public FrmMain() { Ini ...

  5. PHP文件及运行(适合PHP初学者)

    PHP文件可包含HTML.JavaScript代码和 PHP代码,换句话说PHP 代码可以嵌入HTML文档.PHP文件名以php为后缀. PHP代码以"<?php"开头,以& ...

  6. PXE(cobbler)搭建,自动系统安装

    pxe pxe实现全自动安装操作系统,其中用到的软件包括(dhcp首先给客户端分配IP地址提供的系统引导安装系统指向tftp-server启动并加载网络repo仓库由http服务和系统镜像制作) dh ...

  7. Python学习笔记--异常+模块+包

    异常的捕获 基本语法: 示例: 捕获指定异常 基本语法:--必写 示例: 捕获多个异常 示例: 捕获所有异常 示例: 异常else--可写可不写 示例: 异常finally(无论是否出现异常,都会去执 ...

  8. Javaweb学习笔记第七弹

    Maven依赖范围 对于Maven的安装配置等环境准备问题,可详细参考我的前几篇博客, 网址1:https://www.cnblogs.com/liuzijin/p/16654344.html 网址2 ...

  9. 操作系统 && C语言 每日学习记录(day1 ~ day8) 已寄

    现在正式工作了,发现之前学的东西,很多一知半解,不通透,准备再好好系统学一些计算机原理的东西,每天学一学,在这里记录一下. 规划(7.17开始): 同学分享了个超级好的操作系统课程,每天看个一节:ht ...

  10. OVS学习博客推荐

    博客学习 大致看一遍 openvswitch处理upcall过程分析 upcall-cost分析 连接跟踪(conntrack):原理.应用及 Linux 内核实现 ovs upcall处理流程 Li ...