• AC自动机(查询)


            上面讲了AC自动机是如何建树和建自动机的,这里要讲的是AC自动机的查询和各个数组的功能和作用。

            其实AC自动机的查询和KMP算法是及其相近的,都是一个指针跑主串,另一个指针跑ne串(这里就是回跳边)。

            话都说到这了,不妨先来看看ne的真实用处吧。上次有提过,ne数组存的其实就是最长的后缀模式串,当我们找到一个字串在主串中匹配的时候,我们并不能直接继续下去,因为如果两个单词之间存在借位的情况,我们就会忽略从而导致错误,所以我们需要记录下当前字母先前能回跳的位置,这样我们才能将我们所要的字串一网打尽,和KMP其实是完全相同的。

            那么转移边呢,我们说过我们要用一个指针去遍历树,但是我们遍历树的时候一但到头了怎么办,是不是要沿着原路反回。现在我们用一个转移边就可以节省这一部分的操作,那么为什么要把转移边建在自己回跳边的儿子上呢?其实是为了契合上面所建的回跳边,这样我们的跑主串的指针就一定会是不回退的,只需要回跳边不断操作,主串完全就是一个匹配版,不需要进行复杂的操作。

            代码如下:

    1. int query(char *s)
    2. {
    3. int ans = 0;
    4. for(int k = 0, i = 0; s[k]; k ++)
    5. {
    6. i = ch[i][s[k] - 'a'];
    7. for(int j = i; j && ~cnt[j]; j = ne[j])
    8. {
    9. ans += cnt[j];
    10. cnt[j] = -1;
    11. }
    12. }
    13. return ans;
    14. }

            这里需要注意,我们的计数的维护,当我们找到某一个字串后,我们会加上它在Trie树中存储的个数,同时别忘了清零,否则会多加,这里用的是位运算的小技巧。 

            贴一道例题:

    https://www.luogu.com.cn/problem/P3808icon-default.png?t=N7T8https://www.luogu.com.cn/problem/P3808

    题目描述

    给定 𝑛 个模式串 𝑠𝑖 和一个文本串 𝑡,求有多少个不同的模式串在文本串里出现过。
    两个模式串不同当且仅当他们编号不同。

    输入格式

    第一行是一个整数,表示模式串的个数 𝑛。
    第 2 到第 (𝑛+1) 行,每行一个字符串,第 (𝑖+1) 行的字符串表示编号为 𝑖的模式串 𝑠𝑖。
    最后一行是一个字符串,表示文本串 𝑡。

    输出格式

    输出一行一个整数表示答案。

    输入输出样例

    输入 #1复制

    3
    a
    aa
    aa
    aaa

    输出 #1复制

    3

    输入 #2复制

    4
    a
    ab
    ac
    abc
    abcd

    输出 #2复制

    3

    输入 #3复制

    2
    a
    aa
    aa

    输出 #3复制

    2

            代码如下:
     

    1. #include <bits/stdc++.h>
    2. using namespace std;
    3. const int N = 1e6 + 10;
    4. int ch[N][26], cnt[N], ne[N];
    5. int n, idx;
    6. char s[N];
    7. void insert(char *s)
    8. {
    9. int p = 0;
    10. for(int i = 0; s[i]; i ++)
    11. {
    12. int j = s[i] - 'a';
    13. if(!ch[p][j])
    14. ch[p][j] = ++ idx;
    15. p = ch[p][j];
    16. }
    17. cnt[p] ++;
    18. }
    19. void build()
    20. {
    21. queue<int> q;
    22. for(int i = 0; i < 26; i ++)
    23. if(ch[0][i])
    24. q.push(ch[0][i]);
    25. while(!q.empty())
    26. {
    27. int u = q.front();
    28. q.pop();
    29. for(int i = 0; i < 26; i ++)
    30. {
    31. int v = ch[u][i];
    32. if(v)
    33. {
    34. ne[v] = ch[ne[u]][i];
    35. q.push(v);
    36. }
    37. else
    38. ch[u][i] = ch[ne[u]][i];
    39. }
    40. }
    41. }
    42. int query(char *s)
    43. {
    44. int ans = 0;
    45. for(int k = 0, i = 0; s[k]; k ++)
    46. {
    47. i = ch[i][s[k] - 'a'];
    48. for(int j = i; j && ~cnt[j]; j = ne[j])
    49. {
    50. ans += cnt[j];
    51. cnt[j] = -1;
    52. }
    53. }
    54. return ans;
    55. }
    56. int main()
    57. {
    58. cin >> n;
    59. while(n --)
    60. {
    61. cin >> s;
    62. insert(s);
    63. }
    64. build();
    65. cin >> s;
    66. int ans = query(s);
    67. cout << ans << endl;
    68. }

            我发现了,其实这些看起来比较难的算法并没有用什么特别高级的东西,大多都是需要动脑想的,只要将各个数组的作用,以及特殊的定义想明白其实还是简单的。

  • 相关阅读:
    一种简单的三维重建算法的实现【源码】
    【数据库数据恢复】Oracle数据库文件出现坏块报错的数据恢复案例
    Git客户端(TortoiseGit)使用
    AI应用启示录:自动驾驶与“狼来了”的故事
    Vue封装的过度与动画,脚手架配置代理, slot插槽
    对象的比较(下)
    Nodejs和Node-red的关系
    Batch Normalization——李宏毅机器学习笔记
    南京邮电大学运筹学课程实验报告1 线性规划求解 指导
    【推荐收藏】这8个常用缺失值填充技巧一定要掌握
  • 原文地址:https://blog.csdn.net/Qlarkstar/article/details/139434297