Processing math: 100%
0%

AC自动机

AC自动机

一、定义

AC自动机用于多模式匹配,类似树上 kmp 算法。但是 kmp 只有一个模式串,AC 自动机有多个模式串,可以用多个模式串匹配要查询的字符串。

二、原理

AC自动机以Trie树为基础,结合KMP思想建立

  • 构造模式串的Trie树
  • 对Trie树上的所有节点构造失配指针fail(类似kmp中的next数组)

失配指针

多模式串匹配时,假设 pre1,pre2 ,如果原串在 pre1 匹配失败,则会跳到最长的一个 pre1 的后缀处继续匹配。fail指针指向失配串的最长后缀。

fail指针与next数组的异同点

  1. 共同点:两者同样是在失配的时候用于跳转的指针。
  2. 不同点:next 指针求的是最长 Border(即最长的相同前后缀),而 fail 指针指向所有模式串的前缀中匹配当前状态的最长后缀

因为 KMP 只对一个模式串做匹配,而 AC 自动机要对多个模式串做匹配。有可能 fail 指针指向的结点对应着另一个模式串,两者前缀不同。

构造失配指针

考虑字典树中当前的结点 uu 的父节点是 pp 通过字符 c 的边指向 u ,即 trie[p][c]=u 。假设深度小于 u 的所有节点的 fail 指针都已经求出来了。那么现在构造 u 的失配指针。

  • 如果 trie[fail[p]][c] 存在(也就是父节点的失配指针后面加一个字符 c ),那么让 u 的 fail 指针指向 trie[fail[p]][c] 。相当于在 pfail[p] 后面加了一个字符 c ,分别对应 ufail[u]
  • 如果 trie[fail[p]][c] 不存在,则继续向上找 trie[fail[fail[p]]][c] ,贪心的找最长后缀。一直跳到根节点。
  • 如果根节点也不存在,那么就让 fail[u] 指向根节点。

如此就完成了 fail[u] 的构建。

AC_automation_gif_b_3.gif

假设 u=6 ,先找父节点 55 的fail指针指向 10

10 没有 s 边,继续找 10 的fail, 10 的fail指向 0

0 存在 s 边,所以 6 的fail指向 7

finish

构造的代码如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void build() {
for (int i = 0; i < 26; i++)
{
if (tr[0][i]) q.push(tr[0][i]);
}
while (q.size())
{
int u = q.front();
q.pop();
for (int i = 0; i < 26; i++)
{
if (tr[u][i])
{
fail[tr[u][i]] = tr[fail[u]][i];
q.push(tr[u][i]);
}
else
tr[u][i] = tr[fail[u]][i];
}
}
}

构建时使用 BFS 遍历,一次求队列内结点的 fail 指针。注意不将根节点入队,而是将根节点的子节点入队(如果将根节点 root 入队,会将根节点的儿子 u 的fail指针设置为 fail[u]=tr[root][c]=u ,设置成了自身。因此应该把根节点的孩子入队,同时把孩子的fail指针标记为 0 ,可能已经被初始化过了,不用标记)。

然后每次取出队首节点 u ,遍历字符集:

  • 如果 tr[u][i] 存在,将 tr[u][i] 的 fail 指针赋值为 tr[fail[u]][i] 。(本来应该使用while循环,不停向前跳,但是这里通过路径压缩简化了代码,通过else语句代码修改字典树结构,将不存在的字典树的状态链接到了失配指针对应的状态,相当于转移了一个模式串。)这样就得到了自动机。类似编译原理中的自动机。
  • 如果 tr[u][i] 不存在,则令 tr[u][i] 指向 tr[fail[u]][i] ,路径压缩
    AC_automation_gif_b_pro3.gif

匹配查询

匹配查询的代码如下所示:

1
2
3
4
5
6
7
8
9
10
int query(char *t) {
int u = 0, res = 0;
for (int i = 1; t[i]; i++) {
u = tr[u][t[i] - 'a']; // 转移
for (int j = u; j && e[j] != -1; j = fail[j]) {
res += e[j], e[j] = -1;
}
}
return res;
}

每次在字典图上走一个节点,然后访问fail指针,找能够匹配的所有的模式串,加到答案上,然后清零,将该模式串标记为已经统计过了。

复杂度分析:

时间复杂度:定义 |si| 是模板串的长度, |S| 是文本串的长度, || 是字符集的大小(常数,一般为 26)。如果连了 trie 图,时间复杂度就是 O(|si|+n|Σ|+|S|) ,其中 n 是 AC 自动机中结点的数目,并且最大可以达到 O(|si|) 。如果不连 trie 图,并且在构建 fail 指针的时候避免遍历到空儿子,时间复杂度就是 O(|si|+|S|)

例题P3808 【模板】AC 自动机(简单版)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
using namespace std;
using namespace FAST_IO;
const ll mod = 1e9 + 7;
const int INF = 0x3f3f3f3f;
const ll INF_LL = 0x3f3f3f3f3f3f3f3f;
const double eps = 1e-5;
const int maxn = 1e6 + 10;
const int maxm = 1e5 + 10;
int t, n, m, k;
struct ACTrie
{
const static int M = 26;
const static int maxn = 1e6 + 10;
int next[maxn][M] = {}, fail[maxn] = {}, cnt = 0;
int exist[maxn] = {};
queue<int> q;
void insert(char *s)
{
int now = 0, len = strlen(s);
for (int i = 0; i < len; ++i)
{
int c = s[i] - 'a';
if (!next[now][c])
next[now][c] = ++cnt;
now = next[now][c];
}
exist[now]++;
}

void build()
{

for (int i = 0; i < M; ++i)
{
if (next[0][i])
{
fail[next[0][i]] = 0;
q.push(next[0][i]);
}
}
while (q.size())
{
int u = q.front();
q.pop();
for (int i = 0; i < M; ++i)
{
if (next[u][i])
{
fail[next[u][i]] = next[fail[u]][i];
q.push(next[u][i]);
}
else
next[u][i] = next[fail[u]][i];
}
}
}

int query(char *s)
{
int now = 0, len = strlen(s), ans = 0;
for (int i = 0; i < len; ++i)
{
int c = s[i] - 'a';
now = next[now][c];
for (int j = now; j && exist[j] != 0; j = fail[j])
{
ans += exist[j];
exist[j] = 0;
}
}
return ans;
}
};
char a[maxn];
int main()
{
// #define COMP_DATA
#ifndef ONLINE_JUDGE
freopen("in.txt", "r", stdin);
#endif
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
ACTrie trie;
for (int i = 0; i < n; ++i)
{
cin >> a;
trie.insert(a);
}
trie.build();
cin >> a;
cout << trie.query(a) << endl;
return 0;
}

使用node版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
using namespace std;
using namespace FAST_IO;
const ll mod = 1e9 + 7;
const int INF = 0x3f3f3f3f;
const ll INF_LL = 0x3f3f3f3f3f3f3f3f;
const double eps = 1e-5;
const int maxn = 1e6 + 10;
const int maxm = 1e5 + 10;
int t, n, m, k;
struct Node
{
const static int M = 26;
int next[M];
int exist;
int fail;
} trie[maxn];
int cnt = 0;
void insert(char *s)
{
int now = 0, len = strlen(s);
for (int i = 0; i < len; ++i)
{
int c = s[i] - 'a';
if (!trie[now].next[c])
trie[now].next[c] = ++cnt;
now = trie[now].next[c];
}
trie[now].exist++;
}
queue<int> q;
void build()
{
for (int i = 0; i < 26; ++i)
{
int child = trie[0].next[i];
if (child)
{
trie[child].fail = 0;
q.push(child);
}
}
while (q.size())
{
int u = q.front();
q.pop();
for (int i = 0; i < 26; ++i)
{
int child = trie[u].next[i];
if (child)
{
trie[child].fail = trie[trie[u].fail].next[i];
q.push(child);
}
else
trie[u].next[i] = trie[trie[u].fail].next[i];
}
}
}

int query(char *s)
{
int u = 0, len = strlen(s), ans = 0;
for (int i = 0; i < len; ++i)
{
u = trie[u].next[s[i] - 'a'];
for (int j = u; j && trie[j].exist; j = trie[j].fail)
{
ans += trie[j].exist;
trie[j].exist = 0;
}
}
return ans;
}
char a[maxn];
int main()
{
// #define COMP_DATA
#ifndef ONLINE_JUDGE
freopen("in.txt", "r", stdin);
#endif
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
for (int i = 0; i < n; ++i)
{
cin >> a;
insert(a);
}
build();
cin >> a;
cout << query(a) << endl;
return 0;
}

例题P3796 【模板】AC 自动机(加强版)

Powered By Valine
v1.5.1