【AC自动机】AC自动机

2023-03-18,

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自动机的相关教程结束。

《【AC自动机】AC自动机.doc》

下载本文的Word格式文档,以方便收藏与打印。