AC自动机学习笔记

智子

2021-01-01 00:25:59

Personal

更好的阅读体验:[Sophonの博客](https://sophonci117.me/archives/915640971/) ~~再不更新这里都长草了~~ ## 前置知识 - KMP - Trie树 ## 前言 AC自动机是一个字符串匹配算法,与KMP的区别在于,AC自动机可以用$O(\sum |s_i| + |S|)$的复杂度在文本串中同时查找多个模式串,例如[这道题](https://www.luogu.com.cn/problem/P3808) 对于初学者 ~~就是我~~ 而言,可以简单地将AC自动机理解为KMP + Trie树,整个算法分为三步: 1. 将所有的模式串建成一颗Trie树 2. 求出Trie树上每一个节点的失配指针(fail)(类似KMP的next) 3. 将文本串在Trie树上进行匹配 ## 算法过程 ### 建立Trie 这一步和普通的字典树一样,不解释 ```cpp void insert(string& s) { int p = 0; for(int i = 0; i < s.length(); i++) { if(t[p][s[i] - 'a'] == 0) { t[p][s[i] - 'a'] = ++tot; } p = t[p][s[i] - 'a']; } num[p]++; } ``` ### 求失配指针 $fail_u$的定义为,Tire树内所有字符串的所有前缀中,后缀匹配长度最大的的位置;如下图中,$fail_i = j$ (图中不同颜色的边代表字符) ![fail指针](https://cdn.jsdelivr.net/gh/HeliumOI/imghost@latest/AC_fail1.jpg) 如图,节点$i$与节点$j$的后缀匹配最长,所以$fail_i = j$ ![fail指针](https://cdn.jsdelivr.net/gh/HeliumOI/imghost@latest/AC_fail2.jpg) 如图,$fail_i = j$,则$i$的粉色子节点(此处用颜色表示字符)必然也与$j$的粉色子节点有最长的后缀,因此该子节点的$fail$指针就是$k$ 具体求法:对于$u$的子节点$v$,其$fail$指针为$u$的$fail$指针的字符与$v$相同的子节点,证明很简单,因为$fail_u$与$u$的后缀匹配最长,而$v$节点上的字符$i$显然与$trie[fail_u][i]$上的字符$i$相同,所以$fail_v = trie[fail_u][i]$ 理解原理后我们很容易就能求出所有的$fail$指针:在字典树上跑一遍BFS,根节点的子节点的$fail$指针显然为0,所以先将这些点放入队列; 对于每一个当前搜索到的节点$u$,它的$fail$指针肯定已经被求出,所以我们只需要枚举它的所有子节点`t[u][i]`,建立$fail$指针 `fail[t[u][i]] = t[fail[u]][i]` 代码如下: ```cpp void getFail() { //bfs构建fail数组 queue<int> q; for(int i = 0; i < 26; i++) { //根结点下面直接连的第一层结点,fail直接指向根结点0 if(t[0][i]) { q.push(t[0][i]); } } while(!q.empty()) { //队列中维护能够拓展fail值的结点 int u = q.front(); q.pop(); for(int i = 0; i < 26; i++) { if(t[u][i]) { //失配时,以trie[u][i]结尾的后缀尽量在trie中找一个与之相同的前缀(类似KMP) fail[t[u][i]] = t[fail[u]][i]; q.push(t[u][i]); } else { //节点不存在,往上连,最多回到根结点0, 注意是trie不是fail数组 t[u][i] = t[fail[u]][i]; } } } } ``` ### 字符串匹配 这一部分理解难度不大,看代码注释即可: ```cpp int query(string s) { int p = 0, res = 0; //p表示trie中的结点,res表示总和 for(int i = 0; i < s.length(); i++) { p = t[p][s[i] - 'a']; //获取当前节点 for(int j = p; j != 0 && num[j] != -1; j = fail[j]) { //从p开始一直往上跳 res += num[j]; //num[j]表示以当前位置结尾字符串的总数,它们肯定都是 num[j] = -1; //避免重复计算 } } return res; } ``` 用$p$表示将字符串在$Trie$树上匹配时的位置,在每一轮循环中以$p$为起点向上跳,寻找匹配即可 ## 复杂度分析 Trie树的大小为$\sum |s_i|$,建立$fail$指针的复杂度与其相同,遍历文本串需要$O(|S|)$,总时间复杂度为$O(\sum |s_i| + |S|)$ ## 参考资料 [AC 自动机 - OI Wiki](https://oi-wiki.org/string/ac-automaton/)