P5357「【模板】AC自动机(二次加强版)」

1. 题目

题目链接:P5357「【模板】AC自动机(二次加强版)」

题目描述

给你一个文本串 SSnn 个模式串 T1..nT_{1..n},请你分别求出每个模式串 TiT_i 在 SS 中出现的次数。

输入格式

第一行包含一个正整数 nn 表示模式串的个数。

接下来 nn 行,第 ii 行包含一个由小写英文字母构成的字符串 TiT_i

最后一行包含一个由小写英文字母构成的字符串 SS

数据不保证任意两个模式串不相同。

输出格式

输出包含 nn 行,其中第 ii 行包含一个非负整数表示 TiT_iSS 中出现的次数。

输入输出样例

输入 #1

1
2
3
4
5
6
7
5
a
bb
aa
abaa
abaaa
abaaabaa

输出 #1

1
2
3
4
5
6
0
3
2
1

说明/提示

1n2×1051 \leq n \leq 2 \times 10^5T1..nT_{1..n} 的长度总和不超过 2×1052 \times 10^5SS 的长度不超过 2×1062 \times 10^6

2. 题解

分析

  • 普通的查询显然不行(TLE 一片),于是需要考虑如何优化普通的查询。普通的查询导致 TLE 主要原因在于跳 failfail 指针时递归的跳,对于类似 aaaaaaaaaaaa \cdots aaaa 的字符串相当于每向前查找一个字符就需要递归跳 failfail 指针,而每次跳 failfail 只导致深度减 1,最终导致最坏的时间复杂度为 O(nm)O(n*m)(其中 nn 为主串长度,mm 为模式串长度)。

  • 由于整个字典树是确定的,failfail 指针也是确定的,就是说:一个结点如果被更新了,那么字典树中能够匹配到该节点对应字符串的真后缀的结点都是确定的(即递归跳 failfail 到达的结点),在递归跳 failfail 过程中也一定会被更新。既然如此于是我们可以不用那么着急更新所有结点,而是考虑对匹配到最长串的结点打上标记,最后处理完后统一递归跳 failfail 更新所有能够匹配到的结点。

  • 匹配完整个主串后,所有匹配到的最长串的结点都被打上了标记。那此时应该从那些结点开始递归跳 failfail 指针呢?注意到,递归跳 failfail 指针的过程本质上是从树的叶结点走到根结点的过程,这里的树指的是依靠 failfail 指针构建的有向树,根结点就是字典树的根结点(因为 fail[0]=0fail[0] = 0)。于是,对于 failfail 指针构建的有向树而言,其叶结点的入度为 0,出度为 1(一个结点的 failfail 指针指向的位置是固定且唯一的),而我们首先要处理的就是所有叶结点,然后才是叶结点指向的父结点,即将父结点的所有入边关联的子结点处理完后才处理父结点,以此类推,直到根结点,而这正是拓扑排序的顺序。

根据这种思路优化后的最坏复杂度降到了 O(n+m)O(n+m)

代码

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
95
96
97
98
99
100
#include <bits/stdc++.h>
using namespace std;

// AC 自动机
struct Automaton {
#ifndef _AUTOMATON_
#define ll int
#define MAXN 400005
#define MAXCHAR 26
#endif
ll cnt;
ll trie[MAXN][MAXCHAR];
ll exist[MAXN];
ll fail[MAXN];
ll mark[MAXN]; // 模式串编号
ll in[MAXN]; // 入度
Automaton(): cnt(0) {}
// 插入字符串
void insert(char *str, ll i) {
ll p = 0;
for(ll i = 0; str[i]; ++i) {
ll c = str[i] - 'a';
if(!trie[p][c]) {
trie[p][c] = ++cnt;
}
p = trie[p][c];
}
mark[i] = p;
}
// 构建失配指针
void build() {
queue <ll> q;
for(ll i = 0; i < MAXCHAR; ++i) {
if(trie[0][i]) {
q.push(trie[0][i]);
}
}
// BFS
while(q.size()) {
ll p = q.front();
q.pop();
for(ll i = 0; i < MAXCHAR; ++i) {
if(trie[p][i]) {
fail[trie[p][i]] = trie[fail[p]][i];
++in[trie[fail[p]][i]];
q.push(trie[p][i]);
} else {
trie[p][i] = trie[fail[p]][i];
}
}
}
}
// 匹配主串
void match(char *str) {
ll p = 0;
for(ll i = 0; str[i]; ++i) {
ll c = str[i] - 'a';
ll u = trie[p][c];
++exist[u];
p = trie[p][c];
}
}
// 计算所有模式串出现次数
void solve() {
queue <ll> q;
for(ll i = 1; i <= cnt; ++i) {
if(!in[i]) q.push(i);
}
while(q.size()) {
ll p = q.front();
q.pop();
if(fail[p]) {
exist[fail[p]] += exist[p];
--in[fail[p]];
if(!in[fail[p]]) q.push(fail[p]);
}
}
}
};


int main()
{
ll n;
static char str[MAXN*10];
static Automaton ac;
scanf("%d", &n);
for(ll i = 0; i < n; ++i) {
scanf("%s", str);
ac.insert(str, i);
}
scanf("%s", str);
ac.build();
ac.match(str);
ac.solve();
for(ll i = 0; i < n; ++i) {
printf("%d\n", ac.exist[ac.mark[i]]);
}
return 0;
}