You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

195 lines
7.8 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

##[$P3808$ 【模板】$AC$ 自动机(简单版)](https://www.luogu.com.cn/problem/P3808)
## AC自动机详细讲解
$AC$自动机真是个好东西!之前学$KMP$被$Next$指针搞晕了,所以咕了许久都不敢开$AC$自动机,近期学完之后,发现$AC$自动机并不是很难,特别是对于$KMP$​,个人感觉$AC$自动机比$KMP$要好理解一些,可能是因为我对树上的东西比较敏感(实际是因为我到现在都不会$KMP$)。
很多人都说$AC$自动机是在$Trie$树上作$KMP$,我不否认这一种观点,因为这确实是这样,不过对于刚开始学$AC$自动机的同学们就一些误导性的理解(至少对我是这样的)。$KMP$是建立在一个字符串上的,现在把$KMP$搬到了树上,不是很麻烦吗?实际上$AC$自动机只是有$KMP$的一种思想,实际上跟一个字符串的$KMP$有着很大的不同。
所以看这篇$blog$,请放下$KMP$,理解好$Trie$,再来学习。
### 前置技能
* $Trie$(很重要哦)
* $KMP$的思想(懂思想就可以了,不需要很熟练)
### 问题描述
给定$n$个模式串和$1$个文本串,**求有多少个模式串在文本串里出现过**。
<font color='red'><b>注意:是出现过,就是出现多次只算一次</b></font>
默认这里每一个人都已经会了$Trie$。
我们将$n$个模式串建成一颗$Trie$树,建树的方式和建$Trie$完全一样。
<center><img src='https://i.loli.net/2019/05/02/5ccaaa22cbf29.png'></center>
假如我们现在有文本串$ABCDBC$。
我们用文本串在$Trie$上匹配,刚开始会经过$2、3、4$号点,发现到$4$,成功地匹配了一个模式串,然后就不能再继续匹配了,这时我们还要重新继续从根开始匹配吗?
不,这样的效率太慢了。这时我们就要借用$KMP$的思想,从$Trie$上的某个点继续开始匹配。
明显在这颗$Trie$上,我们可以继续从$7$号点开始匹配,然后匹配到$8$。
那么我们怎么确定从那个点开始匹配呢?我们称$i$匹配失败后继续从$j$开始匹配,$j$是$i$的$Fail$**失配指针**)。
### 构建$Fail$指针
**$Fail$的含义**
$Fail$指针的实质含义是什么呢?
如果一个点$i$的$Fail$指针指向$j$。那么$root$到$j$的字符串是$root$到$i$的字符串的一个后缀。
举个例子:(例子来自上面的图)
> $i:4$ $j:7$
> $root$到$i$的字符串是$ABC$
> $root$到$j$的字符串是$BC$
> $BC$是$ABC$的一个后缀
> 所以$i$的$Fail$指针指向$j$
同时我们发现,<font color='red'><b>$C$</b></font> 也是$ABC$的一个后缀。
所以$Fail$指针指的$j$的**深度要尽量大**。
重申一下$Fail$指针的含义:((最长的(当前字符串的后缀))在$Trie$上可以查找到)的末尾编号。
感觉读起来挺绕口的蛤。感性理解一下就好了,没什么卵用的。知道$Fail$有什么用就行了。
### 求$Fail$
首先我们可以确定,每一个点$i$的$Fail$指针指向的点的深度一定是比$i$小的。($Fail$指的是后缀啊)
第一层的$Fail$一定指的是$root$。(比深度$1$还浅的只有$root$了)
设点$i$的父亲$fa$的$Fail$指针指的是$fafail$,那么如果$fafail$有和$i$值相同的儿子$j$,那么$i$的$Fail$就指向$j$。这里可能比较难理解一点,建议画图理解,不过等会转换成代码就很好理解了。
由于我们在处理$i$的情况必须要先处理好$fa$的情况,所以求$Fail$我们使用$BFS$来实现。
### 实现的一些细节
* 1、刚开始我们不是要初始化第一层的$fail$指针为$root$,其实我们可以建一个虚节点$0$号节点,将$0$的所有儿子指向$root$$root$编号为$1$,记得初始化),然后$root$的$fail$指向$0$就$OK$了。效果是一样的。
* 2、如果不存在一个节点$i$,那么我们可以将那个节点设为$fafail$的((值和$i$相同)的儿子)。保证存在性,就算是$0$也可以成功返回到根,因为$0$的所有儿子都是根。
* 3、无论$fafail$存不存在和$i$值相同的儿子$j$,我们都可以将$i$的$fail$指向$j$。因为在处理$i$的时候$j$已经处理好了,如果出现这种情况,$j$的值是第$2$种情况,也是有实际值的,所以没有问题。
* 4、实现时不记父亲我们直接让父亲更新儿子
```c++
void bfs() {
int hh = 0, tt = -1; //将队列的头和尾变量写在这里,可以有效防止多组测试数据的初始化问题
for (int i = 0; i < 26; i++)
if (tr[0][i]) q[++tt] = tr[0][i];
while (hh <= tt) {
int p = q[hh++];
for (int i = 0; i < 26; i++) {
int t = tr[p][i]; // p状态,通过i这条边,到达的新状态t; 也可以理解为是前缀
if (!t)
tr[p][i] = tr[ne[p]][i]; //节点 指向父节点失配指针的i这条边
else {
ne[t] = tr[ne[p]][i]; //失配指针指向父节点失配指针的i这条边
q[++tt] = t; //存在的要入队列
}
}
}
}
```
### 查询
求出了$Fail$指针,查询就变得十分简单了。
为了避免重复计算,我们每经过一个点就打个标记为$-1$,下一次经过就不重复计算了。
同时,如果一个字符串匹配成功,那么他的$Fail$也肯定可以匹配成功(后缀嘛),于是我们就把$Fail$再统计答案,同样,$Fail$的$Fail$也可以匹配成功,以此类推……经过的点累加$flag$,标记为$-1$。
最后主要还是和$Trie$的查询是一样的。
```c++
int res = 0;
for (int i = 0, j = 0; s[i]; i++) {
j = tr[j][s[i] - 'a']; //从j点出发经t这条边,重复利用变量j不断的移动游标j
//沿着失配指针不断向上,累加匹配值
for (int p = j; p && ~cnt[p]; p = ne[p]) res += cnt[p], cnt[p] = -1;
}
```
### 实现代码
```cpp {.line-numbers}
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <iostream>
using namespace std;
const int N = 1000010;
int n; // n个模式串
char s[N]; //模式串
char T[N]; //文本串
// Trie
int tr[N][26], idx;
int cnt[N];
void insert(char *s, int x) {
int p = 0;
for (int i = 0; s[i]; i++) {
int t = s[i] - 'a';
if (!tr[p][t]) tr[p][t] = ++idx;
p = tr[p][t];
}
cnt[p]++; //以p为结束节点的字符串个数+1,如果有重复的,这里++也是OK的~
}
// AC自动机
int q[N], ne[N];
void bfs() {
int hh = 0, tt = -1;
for (int i = 0; i < 26; i++)
if (tr[0][i]) q[++tt] = tr[0][i];
while (hh <= tt) {
int p = q[hh++];
for (int i = 0; i < 26; i++) {
int t = tr[p][i]; //此处直接优化为Trie图,没有采用原始的while向上递归处理的办法,记忆这个版本即可
if (!t)
tr[p][i] = tr[ne[p]][i];
else {
ne[t] = tr[ne[p]][i];
q[++tt] = t;
}
}
}
}
//查询字符串s n个模式串出现了几个
int query(char *s) {
int p = 0;
int res = 0;
for (int i = 0; s[i]; i++) {
p = tr[p][s[i] - 'a'];
for (int j = p; j; j = ne[j]) {
if (cnt[j] == -1) break;
res += cnt[j];
cnt[j] = -1;
}
}
return res;
}
int main() {
//加快读入
ios::sync_with_stdio(false), cin.tie(0);
cin >> n;
//构建Trie树
for (int i = 1; i <= n; i++) {
cin >> s;
insert(s, i);
}
//构建AC自动机
bfs();
//输入文本串
cin >> T;
//输出模式串出现的个数(注意:不是次数,是个数)
printf("%d\n", query(T));
return 0;
}
```