算法学习:AC自动机
【定义】
【自动机】 由 状态集 ,初始状态集 ,终止状态集 ,字母集 ,对应关系五个元素组成的结构
可以简单的将状态集理解为结点,初始状态集理解为初始点,终止状态集理解为终点
字母集理解为一个状态能够拥有的出边的最大个数,而在自动机中,特殊的是,一个结点的所有出边必须都要存在
例如在AC自动机中,每个节点都必须要有26个字母的出边所指向的节点
对应关系,可以理解为连通的边,例:节点 u 的 ’a‘ 的出边能够到达节点 v ,这就是一组对应关系
注:自动机的概念不用知道也可以学自动机,但是个人感觉理解了自动机的含义,更容易去明白他的本质
而且后面学其他自动机也更容易理解
【模式串】 一个比较短的串,需要找 文本串上有多少个他
【文本串】 一个比较长的串,需要在 他上面有多少个模式串
【前置知识】
【trie树(字典树)】从根节点插入一个字符串,依次插入
【KMP】单文本 logn 复杂度内查找模式串出现次数
【强烈建议看最后的扩展】
【解决问题】
多个模式串匹配文本串
给定一个较长串为文本串,给定多个模式串,询问这两者的关系
(也有可能不只有一个文本串)
一般为出现次数什么的
AC自动机本身保存这个字符串的所有子串的相关信息
即这个子串作为模式串出现的次数,这个子串的后缀之中能够包含多少模式串
【算法思想】
这个自动机的优点和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指针建可持久化线段树么
讲解在另外一道题里面