题目描述
给定n个模式串和1个文本串,求有多少个模式串在文本串里出现过。
输入输出格式
输入格式:
第一行一个n,表示模式串个数;
下面n行每行一个模式串;
下面一行一个文本串。
输出格式:
一个数表示答案
输入输出样例
输入样例#1:
2
a
aa
aa
输出样例#1:
2
首先了解
两个会出现在模板题面里的词语
文本串和模式串:
给你几个单词和一个字符串;
求在字符串中出现的单词的个数
这里文本串就是给的字符串,模式串就是单词。
AC自动机是一个多模匹配算法。
学习AC自动机要知道trie字典树和kmp算法。
知道trie和kmp的话AC自动机的算法基本上也就会了
我觉着学AC自动机不是特别理解kmp算法也行来着
然后我们先简单说一下这两个小东西
trie
trie树(字典树),顾名思义,就是一颗树。
那么这颗树用途就像字典一样,用来保存,统计,查找字符串(一般),
它工作原理就和英语字典一样,通过公共前缀储存字符串(公共前缀就是在几个字符串中,前几个相同的字符,比如solve ,solo,这两个字符的公共前缀就是so。)查询字符串;
在建树时我们遍历每一个模式串,
对于一个模式串s,s[i … s.size() - 1], i为树的层数,在每一层我们查找该层中是否有字母与该字符的字母相同,如果有直接找该字母的子节点,如果没有,就新建一个节点,标记为s[i];
同时在每个模式串最后一个字母节点上做一个标记(让标记加一),表示是单词的结尾这样查找时每遍历到一个就让个数加上标记数。
fail指针(kmp)
fail指针其实就是kmp算法在AC自动机里的应用,我的理解就是一个回溯查找,比如在翻字典查一个单词(比如computer)时你记错了翻到了前缀是comm的这页,这时你肯定不是回到c重新去找前缀为c什么的单词而是回到com找前缀为com什么的单词。
fail指针就是告诉你你没找这个单词找失败的时候还可以去尝试找那些类似的单词,减少你重复从前缀开始遍历的过程。
我懒得用途片详解过程
指针并不一定要指针实现,我们也可以用数组。
第一层的指针全指向根节点(根节点为0不表示字母)
然后我们只要记住一句话,就可以掌握fail指针建立的精髓, 就是:
指针指向, 父亲节点的fail指针 >>> 指向节点的 >>> 与该节点表示字母相同的>>> 子节点,若没有,则指向根节点
听起来有点绕我用字符断了一下句,
画的有丶丑。。。
我才不会说这就是我为什么懒得用图片详解的原因
我们可以看一下,下面这个图有点捞前三个毋庸置疑时指向根节点的,然后我们再找第四层的子节点,从e开始,e父亲节点为t那么就看t的指针指的节点,t的fail指针指向了根节点,我们再遍历根节点的子节点(只有一个来着)发现是e,和我们原节点表示字母相同,于是我们就让第四层e的fail指针指向第一层的e;
其他的都是同理得(因为画的时候没有好好想所以剩下的指的都是根节点-_-||)
代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1e6 * 2 + 5;
int trie[MAXN][27];
int cd[MAXN];//单词结尾的标记
int f[MAXN];//fail指针
int cnt = 0;
int n;
string s;
void insert(string s){//添加一个模式串
int root = 0;
for(int i = 0; i < s.size(); ++i){//遍历模式串
int nt = s[i] - 'a' + 1;
if(!trie[root][nt]){//如果该字母节点没有建立
trie[root][nt] = ++cnt;
}
root = trie[root][nt];
// printf("%d\n", root);
}
cd[root] ++;//结尾时标记个数加一
}
void fail(){//找fail指针
queue < int > q;
for(int i = 1; i <= 26; ++i){//遍历26个字母,将第一层的节点全部入队
int x = trie[0][i];
if(x){
f[x] = 0;//fail指向根节点
q.push(x);
}
}
while(!q.empty()){
int x = q.front(); q.pop();
for(int i = 1; i <= 26; ++i){
if(trie[x][i]){//如果是x的子节点
f[trie[x][i]] = trie[f[x]][i];//fail指针指向父亲节点指针指向的字节点
q.push(trie[x][i]);//入队
}
else trie[x][i] = trie[f[x]][i];//如果这个点没有建点,就让该节点为父亲指针指向节点的子节点
}
}
}
int q(string s){//询问
int now = 0, ans = 0;
for(int i = 0; i < s.size(); ++i){
now = trie[now][s[i] - 'a' + 1];//不断的跳fail指针,直到跳到根或出现过的单词
for(int j = now; j && cd[j] != -1; j = f[j]){
ans += cd[j];
cd[j] = -1;
//因为题目大意是一个单词重复出现只算一次,因此每遍历出一个结尾就让其等-1
}
}
return ans;
}
int main()
{
scanf("%d", &n);
for(int i = 1; i <= n; ++i){
cin >> s;
insert(s);
}
f[0] = 0;
fail();
cin >> s;
printf("%d\n", q(s));
// for(int i = 1; i <= n; ++i){
// for(int j = 1; j <= 26; ++j){
// printf("%d ", trie[i][j]);
// }
// }
return 0;
}
了解更多
其他讲解博客
指针版的AC自动机
数组版的AC自动机
yyb大佬的博客
讲的都比我好,我就是看这个学的