来源
$KMP$ 算法是对于一个主串匹配一个子串,$AC$ 自动机则解决了一个主串匹配多个子串的问题。
实现
我们把子串全部建成一棵 $trie$ 树。
在匹配失败后,我们不会再跳回根节点,而是根据节点存储的 $fail$ 数组,跳到 $trie$ 上的一个点继续匹配。
失配指针($fail$)
定义
$trie$ 上的状态 $u$ 的失配指针 $fail_u$ 指向另一个状态 $v$,满足 $v$ 是 $u$ 的最长后缀。
求法
我们令 $trie_{p,c}=u$,那么:
若 $trie_{fail_p,c}$ 存在,则 $fail_u=trie_{fail_p,c}$。
否则就往上找到 $trie_{fail_{fail_p,c}}$,判断是否存在,以此类推,直到跳到根节点,那么 $fail_u$ 就指向根节点。
显然 $fail$ 是按照深度往下遍历的,因此使用 $BFS$。
类似并查集的路径压缩,我们可以在 $trie_{u,c}$ 不存在的情况下,直接将其连接到 $trie_{fail_u,i}$。
1 | inline void build() |
匹配
对于已经匹配到的点,我们把答案加上当前点的串的数量,同时当前点的 $fail$ 的 $fail$ 也是可以的,全部累加起来就是答案了。
可以对于每个已经遍历到的点打标记,以免重复计算。
注意有时会有重复子串。
1 | inline int query(int l, string x) |
优化
在求子串出现数量时,发现暴力跳 $fail$ 极慢无比。
可以在这个点打上标记,最后从下往上累加即可。
那么如何求得累加顺序呢?
我们假设每个点都向其 $fail$ 连一条有向边。
于是就是拓扑排序。
因为 $fail$ 只有一个,每个点的出度也只有一个,就不用再开数组存边了。
入度还是要记的,这样求得起始点后就可以开始快乐 $topo$,同时累加答案。
快了好多。
1 | inline void query(int l, string x) |