1. 题目
题目链接:P5357「【模板】AC自动机(二次加强版)」 。
题目描述
给你一个文本串 S 和 n 个模式串 T1..n,请你分别求出每个模式串 Ti 在 SS 中出现的次数。
输入格式
第一行包含一个正整数 n 表示模式串的个数。
接下来 n 行,第 i 行包含一个由小写英文字母构成的字符串 Ti 。
最后一行包含一个由小写英文字母构成的字符串 S 。
数据不保证任意两个模式串不相同。
输出格式
输出包含 n 行,其中第 i 行包含一个非负整数表示 Ti 在 S 中出现的次数。
输入输出样例
输入 #1
1 2 3 4 5 6 7
| 5 a bb aa abaa abaaa abaaabaa
|
输出 #1
说明/提示
1≤n≤2×105,T1..n 的长度总和不超过 2×105,S 的长度不超过 2×106 。
2. 题解
分析
-
普通的查询显然不行(TLE 一片),于是需要考虑如何优化普通的查询。普通的查询导致 TLE 主要原因在于跳 fail 指针时递归的跳,对于类似 aaaa⋯aaaa 的字符串相当于每向前查找一个字符就需要递归跳 fail 指针,而每次跳 fail 只导致深度减 1,最终导致最坏的时间复杂度为 O(n∗m)(其中 n 为主串长度,m 为模式串长度)。
-
由于整个字典树是确定的,fail 指针也是确定的,就是说:一个结点如果被更新了,那么字典树中能够匹配到该节点对应字符串的真后缀的结点都是确定的(即递归跳 fail 到达的结点),在递归跳 fail 过程中也一定会被更新。既然如此于是我们可以不用那么着急更新所有结点,而是考虑对匹配到最长串的结点打上标记,最后处理完后统一递归跳 fail 更新所有能够匹配到的结点。
-
匹配完整个主串后,所有匹配到的最长串的结点都被打上了标记。那此时应该从那些结点开始递归跳 fail 指针呢?注意到,递归跳 fail 指针的过程本质上是从树的叶结点走到根结点的过程,这里的树指的是依靠 fail 指针构建的有向树,根结点就是字典树的根结点(因为 fail[0]=0)。于是,对于 fail 指针构建的有向树而言,其叶结点的入度为 0,出度为 1(一个结点的 fail 指针指向的位置是固定且唯一的),而我们首先要处理的就是所有叶结点,然后才是叶结点指向的父结点,即将父结点的所有入边关联的子结点处理完后才处理父结点,以此类推,直到根结点,而这正是拓扑排序的顺序。
根据这种思路优化后的最坏复杂度降到了 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;
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]); } } 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; }
|