【定义】
【自动机】 由 状态集 ,初始状态集 ,终止状态集 ,字母集 ,对应关系五个元素组成的结构
可以简单的将状态集理解为结点,初始状态集理解为初始点,终止状态集理解为终点
字母集理解为一个状态能够拥有的出边的最大个数,而在自动机中,特殊的是,一个结点的所有出边必须都要存在
例如在AC自动机中,每个节点都必须要有26个字母的出边所指向的节点
对应关系,可以理解为连通的边,例:节点 u 的 ’a‘ 的出边能够到达节点 v ,这就是一组对应关系
注:自动机的概念不用知道也可以学自动机,但是个人感觉理解了自动机的含义,更容易去明白他的本质
而且后面学其他自动机也更容易理解
【模式串】 一个比较短的串,需要找 文本串上有多少个他
【文本串】 一个比较长的串,需要在 他上面有多少个模式串
【前置知识】
【trie树(字典树)】从根节点插入一个字符串,依次插入
【KMP】单文本 logn 复杂度内查找模式串出现次数
【强烈建议看最后的扩展】
【解决问题】
多个模式串匹配文本串
给定一个较长串为文本串,给定多个模式串,询问这两者的关系
(也有可能不只有一个文本串)
一般为出现次数什么的
【算法思想】
这个自动机的优点和KMP类似,所以有的博客中也会说,这是一个树上KMP。
KMP的优点在于有next数组作为指针失配时的指针,使字符串匹配可以不需要再次查找已经查找的串
而AC自动机也有他的“next数组”,f a i l 。
AC自动机中的 f a i l 指针表示的是,当当前匹配的模式串失效后,已经匹配的一半模式串的拥有最长后缀的模式串的结尾的位置
对于一个AC自动机,我们需要先把所有的模式串都插入一颗 trie 树
int trie[MAXN][26];
//s是需要插入的模式串
void insert(string s)
{
int u = 0;
//从根节点开始检索
for (int i = 0; i < s.size(); i++)
{
int x = s[i] - 'a';
if (!trie[u][x])
//如果没有这个节点
//需要新开拓一个节点,保存这个新的分支
{
trie[u][x] = ++cnt;
}
u = trie[u][x];
}
val[u]++;
//说明这个节点是一个模式串的结尾
return;
}
然后根据我们对 f a i l 的定义去寻找f a i l 指针
void get_fail()
{
queue<int> q;
for (int i = 0; i < 26; i++)
if (trie[0][i]) fail[trie[0][i]] = 0, q.push(trie[0][i]);
//让所有的根节点连接的节点的fail指针指向根节点
//并且加入队列
while (!q.empty())
{
int u = q.front(); q.pop();
for (int i = 0; i < 26; i++)
//循环查找每个字母
if (trie[u][i])
//如果存在这个节点
//
{
//他的 fail 就是 他父节点的 fail 的这个字母的位置
//因为等于是在后缀上新加了一个字母
fail[trie[u][i]] = trie[fail[u]][i];
q.push(trie[u][i]);
}
else
trie[u][i] = trie[fail[u]][i];
//如果没有
//向上递归一层,方便之后的查找
}
return;
}
这里应该有张图说明一下,但是我懒得画
【模板题】
【题目大意】给n个模式串和1个文本串,问有多少个模式串在文本串中出现过
【解决方法】在trie树上跑文本串
int query(string s)
{
int u = 0, ans = 0;
//从根节点开始依次查找
for (int i = 0; i < s.size(); i++)
{
u = trie[u][s[i] - 'a'];
//走到自己这个位置字符所在的节点
for (int t = u; t && ~val[t]; t = fail[t])
//从这个节点开始向上跳fail指针
//查找有没有符合要求的字符串,如果是则这个字符串就会被记录
ans += val[t], val[t] = -1;
//-1是为了防止被重复计算
}
return ans;
}

#include<cstdio>
#include<iostream>
#include<string>
#include<queue>
using namespace std;
const int MAXN = 1000010;
int fail[MAXN],cnt;
int trie[MAXN][26];
int val[MAXN];
void insert(string s)
{
int u = 0;
for (int i = 0; i < s.size(); i++)
{
int x = s[i] - 'a';
if (!trie[u][x])
{
trie[u][x] = ++cnt;
}
u = trie[u][x];
}
val[u]++;
return;
}
void get_fail()
{
queue<int> q;
for (int i = 0; i < 26; i++)
if (trie[0][i]) fail[trie[0][i]] = 0, q.push(trie[0][i]);
while (!q.empty())
{
int u = q.front(); q.pop();
for (int i = 0; i < 26; i++)
if (trie[u][i])
{
fail[trie[u][i]] = trie[fail[u]][i];
q.push(trie[u][i]);
}
else
trie[u][i] = trie[fail[u]][i];
}
return;
}
int query(string s)
{
int u = 0, ans = 0;
for (int i = 0; i < s.size(); i++)
{
u = trie[u][s[i] - 'a'];
for (int t = u; t && ~val[t]; t = fail[t])
ans += val[t], val[t] = -1;
}
return ans;
}
int main()
{
int T;
cin >> T;
while (T--)
{
string s;
cin >> s;
insert(s);
}
get_fail();
string s;
cin >> s;
printf("%d", query(s));
return 0;
}
这一步扩展其实才是AC自动机的精髓所在,也是比赛中比较常用的方法
就像是网络流不可能直接给图找最大流,而是通过建图的方式考察对算法的理解
AC自动机也有同样的情况
那就是通过抽离 fail 指针,建 fail 树,并且对其进行一系列操作完成任务
这不就是AC自动机抽离fail指针建可持久化线段树么
讲解在另外一道题里面
