[关闭]
@WangYixu 2018-06-22T07:35:05.000000Z 字数 2231 阅读 996

AC自动机

OI 笔记 算法 字符串


一句话总结

AC自动机 = Trie + KMP

主要流程

注意:之后的讲解中刻意混淆了“指针”与“指针指向的元素”

这里,以一组数据为例:

模板串:

  1. she
  2. he
  3. say
  4. shr
  5. her

文本串

  1. yasherhs

本文将以图片+代码的形式呈现,如有不理解之处,手动模拟尝试即可。

数据结构

  1. int Trie_[MAXN_][26]; // Trie树
  2. int Fail_[MAXN_]; // 失配指针
  3. int Cnt_[MAXN_]; // 以该节点为单词结尾的单词数
  4. int tot_;
  5. int q_[MAXN_ + 10], qhd_, qtl_;

建立Trie树

1.建立Trie树

  1. void add(char *S_) { // 添加模板串
  2. int pos_ = 0; // 记录当前节点位置,从根开始
  3. int len_ = strlen(S_); // 插入的串的长度
  4. for (Rint i = 0; i < len_; ++i) { // 逐字符检查
  5. if (!Trie_[ pos_ ][ S_[i] - 'a' ]) // 如果没有这个位置
  6. Trie_[pos_][ S_[i] - 'a'] = ++tot_; // 申请这个位置
  7. pos_ = Trie_[pos_][ S_[i] - 'a']; // 向下走
  8. }
  9. Cnt_[pos_]++; // 记录出现次数
  10. }

建立失配指针

2.建立失配指针

  1. void build() { // 构建失配指针(Fail[])
  2. // 广度优先遍历
  3. qhd_ = qtl_ = 0;
  4. Fail_[0] = 0;
  5. // 首先处理模板串起点的失配指针(=0)
  6. for (Rint i = 0; i < 26; ++i) {
  7. if (Trie_[0][i]) { // 如果有该节点
  8. Fail_[ Trie_[0][i] ] = 0;
  9. q_[qtl_++ % QLEN_] = Trie_[0][i]; // 入队
  10. }
  11. }
  12. int tmp_;
  13. while (qhd_ != qtl_) {
  14. tmp_ = q_[qhd_++ % QLEN_];
  15. for (int i = 0; i < 26; ++i) { // 遍历子节点
  16. if (Trie_[tmp_][i]) { // 该子节点存在
  17. Fail_[ Trie_[tmp_][i] ] = Trie_[ Fail_[tmp_] ][i];
  18. // 失配指针为 当前节点的失配指针 的子节点
  19. q_[qtl_++ % QLEN_] = Trie_[tmp_][i];
  20. } else { // 该子节点不存在
  21. // 直接连接到失配指针的子节点
  22. // 现在,trie树成了trie图
  23. Trie_[tmp_][i] = Trie_[ Fail_[tmp_] ][i];
  24. }
  25. }
  26. }
  27. }

这里还有一个小优化:将trie树变成trie图,减少失配指针跳跃次数。

查询

极其类似KMP
(这里图示有误,will fix soon)

  1. // 统计文本串中出现了多少种模板串
  2. int query(char* S_) {
  3. int ans_ = 0, now_ = 0;
  4. int len_ = strlen(S_);
  5. for (Rint i = 0; i < len_; ++i) {
  6. now_ = Trie_[now_][S_[i] - 'a'];
  7. for (register unsigned int t = now_; t && ~Cnt_[t]; t = Fail_[t]) {
  8. ans_ += Cnt_[t];
  9. Cnt_[t] = -1;
  10. }
  11. }
  12. return ans_;
  13. }

例题

[POI2000]病毒

这个题堪称经典AC自动机例题,做完后对AC自动机的了解又上了一个层次。

考虑文本串匹配的过程,如果一直无法匹配到单词终点,那么,AC自动机上一定有环,参考下图理解环的意义。

该图中,蓝色边为原始trie树,黑色边为fail指针,红色边为trie图改进,这里为了使图没有那么乱,没有为末尾节点建立trie图,图中的环已经被加粗。

要注意的是,一个点的失配指针如果是一个单词末尾,那么这个点也是单词末尾,对应程序中的:

val[ trie[tmp][i] ] |= val[ trie[fail[tmp]][i] ];

code:

  1. #define Rint register int
  2. int trie[M][2], fail[M], val[M], tot;
  3. char s[M];
  4. int n;
  5. int q[M], qhd, qtl;
  6. int flag, vis[M], ins[M];
  7. inline void add(char *s);
  8. inline void build() {
  9. for (Rint i = 0; i <= 1; ++i) {
  10. if (trie[0][i]) {
  11. fail[ trie[0][i] ] = 0;
  12. q[qtl++] = trie[0][i];
  13. }
  14. }
  15. int tmp;
  16. while (qhd != qtl) {
  17. tmp = q[qhd++];
  18. for (Rint i = 0; i <= 1; ++i) {
  19. if (trie[tmp][i]) {
  20. fail[ trie[tmp][i] ] = trie[ fail[tmp] ][i];
  21. val[ trie[tmp][i] ] |= val[ trie[fail[tmp]][i] ];
  22. q[qtl++] = trie[tmp][i];
  23. } else {
  24. trie[tmp][i] = trie[ fail[tmp] ][i];
  25. }
  26. }
  27. }
  28. }
  29. void dfs(int f) {
  30. if (flag) return;
  31. ins[f] = 1;
  32. vis[f] = 1;
  33. for (int i = 0; i <= 1; ++i) {
  34. if (trie[f][i] && !val[ trie[f][i] ]) {
  35. if (ins[ trie[f][i] ]) {
  36. flag = 1;
  37. break;
  38. }
  39. if (!vis[ trie[f][i] ]) dfs(trie[f][i]);
  40. }
  41. }
  42. ins[f] = 0;
  43. }
  44. int main() {
  45. // read and build
  46. dfs(0);
  47. if (flag) printf("TAK\n");
  48. else printf("NIE\n");
  49. return 0;
  50. }
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注