Definition & Solution

AC自动机是一种多模式串的字符串匹配数据结构,核心在于利用 fail 指针在失配时将节点跳转到当前节点代表字符串的最长后缀子串。

首先对 模式串 建出一棵 tire 树,考虑树上以根节点为一个端点的每条链显然都对应着某一模式串的一个前缀子串,以下以树上的每个节点来代指从根节点到该节点对应的字符串。

定义一个字符串 \(S\) 在 trie 树上“出现过”当且仅当存在一条以根节点为一个端点的链,该链的对应字符串为 \(S\)。

考虑对每个节点求出一个 fail 指针,该指针指向在树上出现的该子串的 最长 后缀子串的端点。考虑在匹配文本串的时候,如果某一位置失配,最优的选择显然是跳转到被匹配串的最长后缀子串。因为这样所有在树上出现过的字符串都有机会被跳转到。

需要注意的是如果一个字符串匹配到了文本串,那么他的所有后缀子串都能匹配文本串。也就是说对于一个节点,他的fail,fail的fail,一直到根节点都能匹配当前文本串。

考虑求出fail指针的方法:

设根节点为空,显然根节点的所有孩子的fail指着指向根节点。

对于一个已经求出 fail 指针的节点 \(u\),设 \(u\) 的 fail 指向 \(w\),考虑 \(u\) 的一个孩子 \(v\),设 \(w\) 对应的孩子为 \(z\),且设 \(z\) 在 trie 树上是真实存在的。由于 \(w\) 是 \(u\) 的最长后缀子串,显然 \(w\) 的对应孩子 \(z\) 是 \(v\) 的最长后缀子串,于是直接将 \(v\) 的 fail 指向 \(z\) 即可。考虑如果 \(v\) 在 fail 上是不存在的,那么考虑一个 fail 指针指向 \(u\) 的节点,它对应 \(v\) 的指针显然应该指向 \(u\) 对应子串加上 \(v\) 代表字符后的最长真实存在的后缀子串。显然这个位置是 \(z\)。为了匹配时方便,我们直接将 \(u\) 的子节点指针指向 \(z\),这样在匹配 fail 指针指向 \(u\) 的节点时即对应第一种情况,正确性已经得到了证明。

于是一次 BFS 即可解决问题,对于 \(u\) 的子节点 \(v\) ,如果 \(v\) 真是存在,则将 \(v\) 的 fail 指针指向 \(u\) 的 fail 的对应节点,否则将 \(v\) 指向 \(u\) 的 fail 的对应子节点。

需要注意的是,如果一个节点再加上一个字符后在树上不存在任何一个后缀子串,那么该最长后缀为空,应该指向根节点。所以在初始化时,应该将所有节点的孩子和 fail 都指向根节点。

Samples

【P3808】AC自动机(简单版)

Description

给定 \(n\) 个模式串 \(S\) 和\(1\)个文本串 \(T\),求有多少个模式串在文本串里出现过。

Limitation

模式串总长度和文本串长度都不超过 \(10^6\)

Solution

考虑建出自动机后,在树上按照文本串匹配,注意到每匹配到一个节点,他的所有后缀子串都出现过,于是在每个节点都应该不断跳 fail 直到根,一路上的子串都标记为出现。

注意到本题只问有多少个串出现,而没有问每个串出现多少次,所以如果一个字符串已经在之前被跳到过了,他的所有后缀子串显然在之前也都已经被跳到过了,所以每跳到一个节点对该节点打一下标记,如果跳到过该节点了就直接break即可。

考虑一个节点最多会被跳一次,一共有 \(O(\Sigma|S|)\) 个节点,同时建立自动机的复杂度是 \(O(\Sigma|S|)\) 的,另外匹配文本串的复杂度是 \(O(|T|)\) 的,于是总时间复杂度 \(O(|T| + \Sigma|S|)\)

Code

#include <cstdio>
#include <cstring>
#include <queue>
#ifdef ONLINE_JUDGE
#define freopen(a, b, c)
#endif typedef long long int ll; namespace IPT {
const int L = 1000000;
char buf[L], *front=buf, *end=buf;
char GetChar() {
if (front == end) {
end = buf + fread(front = buf, 1, L, stdin);
if (front == end) return -1;
}
return *(front++);
}
} template <typename T>
inline void qr(T &x) {
char ch = IPT::GetChar(), lst = ' ';
while ((ch > '9') || (ch < '0')) lst = ch, ch=IPT::GetChar();
while ((ch >= '0') && (ch <= '9')) x = (x << 1) + (x << 3) + (ch ^ 48), ch = IPT::GetChar();
if (lst == '-') x = -x;
} namespace OPT {
char buf[120];
} template <typename T>
inline void qw(T x, const char aft, const bool pt) {
if (x < 0) {x = -x, putchar('-');}
int top=0;
do {OPT::buf[++top] = static_cast<char>(x % 10 + '0');} while (x /= 10);
while (top) putchar(OPT::buf[top--]);
if (pt) putchar(aft);
} const int maxt = 26;
const int maxn = 1000009; struct Tree {
Tree *son[maxt], *fail;
int endtime;
bool vis; Tree(Tree *const _rt) : endtime(0), vis(false) {
for (auto &u : son) u = _rt;
fail = _rt;
} Tree() : endtime(0), vis(false) {
fail = this;
for (auto &u : son) u = this;
}
};
Tree rot;
Tree *rt = &rot; int n, ans, pcnt = 0;
char MU[maxn];
std::queue<Tree*>Q; void makefail();
void ReadStr(char *s);
void query(const char *s);
void insert(const char *s); int main() {
freopen("1.in", "r", stdin);
qr(n);
while (n--) {
ReadStr(MU); insert(MU);
}
makefail();
ReadStr(MU); query(MU);
return 0;
} void ReadStr(char *s) {
do *s = IPT::GetChar(); while ((*s == ' ') || (*s == '\n') || (*s == '\r'));
do *(++s) = IPT::GetChar(); while ((~*s) && (*s != ' ') && (*s != '\n') && (*s != '\r'));
*s = 0;
} void insert(const char *s) {
auto u = &rot;
while (*s) {
int k = *(s++) - 'a';
u = u->son[k] != rt? u->son[k] : u->son[k] = new Tree(&rot);
}
++u->endtime;
} void makefail() {
for (auto u : rot.son) if (u != rt) {
Q.push(u);
}
while (!Q.empty()) {
auto u = Q.front(); Q.pop();
for (auto &v : u->son) {
auto k = &v - u->son;
if (v != rt) {
v->fail = u->fail->son[k];
Q.push(v);
} else {
v = u->fail->son[k];
}
}
}
} void query(const char *s) {
auto u = &rot;
while (*s) {
u = u->son[*(s++) - 'a'];
for (auto v = u; v->vis == false; v = v->fail) {
v->vis = true;
ans += v->endtime;
}
}
qw(ans, '\n', true);
}

【P3706】AC自动机(加强版)

Description

给定 \(n\) 个模式串 \(S\) 和一个文本串 \(T\),\(S\) 可能在 \(T\) 中出现多次,求出现最多的是哪些模式串,出现了多少次。

Limitation

\(1~\leq~n~\leq~150\)

\(|S|~\leq~70,~|T|~\leq~10^6\)

Solution

暴力的想法显然是建出AC自动机然后每匹配到一个节点就暴力跳 fail,考虑本题与上一题的区别在于本题的模式串每出现一次就要统计一次,所以每个节点必须跳 fail 一直到根。考虑一个字符串 \(S\) 的后缀子串个数显然是 \(O(|S|)\) 的,匹配文本串的复杂度是 \(O(|T|)\) 的,于是总复杂度 \(O(|S||T|)\) 的。显然很不优秀。

考虑 AC 自动机的一个神奇性质:将所有的 fail 指针连成边,构成了一棵树。

证明:

考虑除了根节点以外每个点都有且仅有一个 fail 指针,根节点没有 fail 指针,这个条件等价于图上有 \(n-1\) 条边。

又由于 tire 树是联通的,所以该图满足 “联通”,“有 \(n-1\) 条边” 两个特性,根据树的判定定理可以证明这是一棵树。QED。

于是考虑跳 fail 一直到根将路径上的标记+1等价于将某个节点到根的链上所有点的标记整体加一,这个过程显然可以树形DP完成,于是每次在该节点打一个+1的标记即可。每个点的真实标记值为孩子的真是标记值之和加上该节点的标记值。

于是总复杂度 \(O(|T|~+~\Sigma|S|)\)

Code

#include <cstdio>
#include <queue>
#include <vector>
#include <algorithm>
#ifdef ONLINE_JUDGE
#define freopen(a, b, c)
#endif typedef long long int ll; namespace IPT {
const int L = 1000000;
char buf[L], *front=buf, *end=buf;
char GetChar() {
if (front == end) {
end = buf + fread(front = buf, 1, L, stdin);
if (front == end) return -1;
}
return *(front++);
}
} template <typename T>
inline void qr(T &x) {
char ch = IPT::GetChar(), lst = ' ';
while ((ch > '9') || (ch < '0')) lst = ch, ch=IPT::GetChar();
while ((ch >= '0') && (ch <= '9')) x = (x << 1) + (x << 3) + (ch ^ 48), ch = IPT::GetChar();
if (lst == '-') x = -x;
} namespace OPT {
char buf[120];
} template <typename T>
inline void qw(T x, const char aft, const bool pt) {
if (x < 0) {x = -x, putchar('-');}
int top=0;
do {OPT::buf[++top] = static_cast<char>(x % 10 + '0');} while (x /= 10);
while (top) putchar(OPT::buf[top--]);
if (pt) putchar(aft);
} const int maxm = 75;
const int maxn = 155;
const int maxt = 26;
const int maxL = 1000005; struct Tree *rot; struct Tree {
Tree *son[maxt], *fail;
std::vector<int>Endid;
std::vector<Tree*>tson;
bool vistag;
int vistime; Tree() {
for (auto &u : son) u = rot;
fail = rot;
vistag = false;
vistime = 0;
} ~Tree() {
this->vistag = false;
for (auto u : son) if (u->vistag) delete u;
}
}; int n, maxv;
char MU[maxn][maxm], CU[maxL];
std::queue<Tree*>Q;
std::vector<int>ans; void init();
void work();
void clear();
void print();
void buildfail();
void ReadStr(char *s);
void dfs(Tree *const s);
bool IsLet(const char *const s);
void Inserot(const char *s, const int id); int main() {
freopen("1.in", "r", stdin);
qr(n);
while (n) {
clear();
init();
buildfail();
work();
print();
n = 0; qr(n);
}
return 0;
} void clear() {
delete rot;
maxv = 0; ans.clear();
} void init() {
rot = new Tree;
for (auto &u : rot->son) u = rot;
rot->fail = rot;
for (int i = 1; i <= n; ++i) {
ReadStr(MU[i]);
Inserot(MU[i], i);
}
} void ReadStr(char *s) {
do *s = IPT::GetChar(); while (!IsLet(s));
do *(++s) = IPT::GetChar(); while (IsLet(s));
*s = 0;
} inline bool IsLet(const char *const s) {
return (*s >= 'a') && (*s <= 'z');
} void Inserot(const char *s, const int id) {
auto u = rot;
while (*s) {
int k = *(s++) - 'a';
u = u->son[k] != rot ? u->son[k] : u->son[k] = new Tree;
}
u->Endid.push_back(id);
} void buildfail() {
for (auto u : rot->son) if (u != rot) Q.push(u);
while (!Q.empty()) {
auto u = Q.front(); Q.pop();
for (auto &v : u->son) {
auto k = &v - u->son;
if (v != rot) {
v->fail = u->fail->son[k];
Q.push(v);
} else {
v = u->fail->son[k];
}
}
}
for (auto &u : rot->son) if (u != rot) {
u->vistag = true; Q.push(u);
}
while (!Q.empty()) {
auto u = Q.front(); Q.pop();
u->fail->tson.push_back(u);
for (auto &v : u->son) if ((v != rot) && (v->vistag == false)) {
v->vistag = true; Q.push(v);
}
}
} void work() {
ReadStr(CU);
auto s = CU;
auto u = rot;
while (*s) {
int k = *(s++) - 'a';
++((u = u->son[k])->vistime);
}
dfs(rot);
} void dfs(Tree *const u) {
for (auto v : u->tson) {
dfs(v);
u->vistime += v->vistime;
}
if (u->Endid.size()) {
if (u->vistime > maxv) {
maxv = u->vistime;
ans.clear();
for (auto i : u->Endid) ans.push_back(i);
} else if (u->vistime == maxv) {
for (auto i : u->Endid) ans.push_back(i);
}
}
} void print() {
std::sort(ans.begin(), ans.end());
qw(maxv, '\n', true);
for (auto i : ans) printf("%s\n", MU[i]);
}

【AC自动机】AC自动机的更多相关文章

  1. 后缀自动机/回文自动机/AC自动机/序列自动机----各种自动机(自冻鸡) 题目泛做

    题目1 BZOJ 3676 APIO2014 回文串 算法讨论: cnt表示回文自动机上每个结点回文串出现的次数.这是回文自动机的定义考查题. #include <cstdlib> #in ...

  2. AC自动机(AC automation)

    字典树+KMP 参考自: http://www.cppblog.com/mythit/archive/2009/04/21/80633.html ; //字典大小 //定义结点 struct node ...

  3. 【专题】字符串专题小结(AC自动机 + 后缀自动机)

    AC自动机相关: $fail$树: $fail$树上以最长$border$关系形成父子关系,我们定一个节点对应的串为根到该节点的路径. 对于任意一个非根节点$x$,定$y = fa_{x}$,那$y$ ...

  4. HDU - 6208 The Dominator of Strings HDU - 6208 AC自动机 || 后缀自动机

    https://vjudge.net/problem/HDU-6208 首先可以知道最长那个串肯定是答案 然后,相当于用n - 1个模式串去匹配这个主串,看看有多少个能匹配. 普通kmp的话,每次都要 ...

  5. BZOJ4032 [HEOI2015]最短不公共子串 【后缀自动机 + 序列自动机 + dp】

    题目链接 BZOJ4032 题解 首先膜\(hb\) 空手切神题 一问\(hash\),二问枚举 三问\(trie\)树,四问\(dp\) 南二巨佬神\(hb\) 空手吊打自动机 \(orz orz ...

  6. bzoj 3796: Mushroom追妹纸 AC自动机+后缀自动机+dp

    题目大意: 给定三个字符串s1,s2,s3,求一个字符串w满足: w是s1的子串 w是s2的子串 s3不是w的子串 w的长度应尽可能大 题解: 首先我们可以用AC自动机找出s3在s1,s2中出现的位置 ...

  7. BZOJ2754: [SCOI2012]喵星球上的点名(AC自动机/后缀自动机)

    Description a180285幸运地被选做了地球到喵星球的留学生.他发现喵星人在上课前的点名现象非常有趣.   假设课堂上有N个喵星人,每个喵星人的名字由姓和名构成.喵星球上的老师会选择M个串 ...

  8. AC自动机&后缀自动机

    理解的不够深 故只能以此来加深理解 .我这个人就是蠢没办法 学长讲的题全程蒙蔽.可能我字符串就是菜吧,哦不我这个人就是菜吧. AC自动机的名字 AC 取自一个大牛 而自动机就比较有讲究了 不是寻常的东 ...

  9. 字符串[未AC](后缀自动机):HEOI 2016 str

    超级恶心,先后用set维护right,再用主席树维护,全部超时,本地测是AC的.放心,BZOJ上还是1S限制,貌似只有常数优化到一定境界的人才能AC吧. 总之我是精神胜利了哦耶QAQ #include ...

  10. HDU3065【AC自动机-AC感言】

    Fourth AC zi dong ji(Aho-Corasick Automation) of life 9A(其实不止交了10发...) 感言: 一开始多组数据这种小数据还是...无伤大局,因为改 ...

随机推荐

  1. 织梦调用多个栏目typeid="1,2,3"不支持的解决方法

    织梦arclist调用副栏目不显示的解决办法: 打开/include/taglib/arclist.lib.php,代码约位于295-296行,查找以下两行代码: if($CrossID=='') $ ...

  2. Python模块xlwt对excel进行写入操作

    python常用模块目录 1.安装 $ pip install xlwt 2.创建表格和工作表单写入内容 例子: import xlwt # 创建一个workbook 设置编码 workbook = ...

  3. java BufferedWriter写数据不完全

    package com.brucekun.keyword; import java.io.BufferedReader; import java.io.BufferedWriter; import j ...

  4. [Elite 2008 Dec USACO]Jigsaw Puzzles

    #include <iostream> #include <cstdio> #include <cstring> using namespace std; #def ...

  5. 个人作业2--APP案例分析

    产品 选择产品:酷狗音乐播放器 版本:Android版 选择理由:是我高中就开始用的音乐播放软件,在平时使用频率比较高,平时喜欢在累的时候听音乐放松. 调研 第一次上手体验 第一次使用的时候,感觉整个 ...

  6. 08_Java基础语法_第8天(Eclipse)_讲义

    今日内容介绍 1.Eclipse开发工具 2.超市库存管理系统 01Eclipse的下载安装 * A: Eclipse的下载安装  * a: 下载 * http://www.eclipse.org ...

  7. Gradle入门(1):安装

    在Ubuntu下,执行以下命令: sudo apt-get install gradle 安装完成后,执行命令: gradle -v 得到以下信息: Picked up _JAVA_OPTIONS:  ...

  8. 浅谈iOS内存管理机制

    iOS内存管理机制的原理是引用计数,引用计数简单来说就是统计一块内存的所有权,当这块内存被创建出来的时候,它的引用计数从0增加到1,表示有一个对象或指针持有这块内存,拥有这块内存的所有权,如果这时候有 ...

  9. 实验一 命令解释程序cmd的编写

    #include<stdio.h>#include<stdlib.h>#include<string.h>#define N 30main(){ char str[ ...

  10. double 和 im2double 的区别

    double 就是简单地把一个变量类型转换成double型,数值大小不变. 函数im2double将输入换成double类型.如果输入是unit8,unit16或者是二值的logical类型,则函数i ...