AC自动机(详解)

AC自动机(详解)需要提前了解的知识 KMP 字典树例题 传送门 AC 自动机是 KMP 和字典树的结合体 如果说 KMP 用于单模式匹配 那么 AC 自动机是用于多模式匹配的 举个例子 KMP 适用于在一篇文章中找一句话 AC 自动机适合在一篇文章中寻找多句话 并不是简单的多用几次 KMP KMP 算法构建了一个查询表 nex 数组 我们可以按照相同的思路构建 nex 表 在 ac 自动机中成为失配数组 算法思路 先将所有的模式串建立字典树 建立 nex 数组 然后就开始匹配

需要提前了解的知识:KMP, 字典树
由于类比了kmp算法,建议大家可以看一下我的kmp教程,以便于理解kmp(详解)

简介
AC自动机是KMP和trie的结合体。KMP算法适用于单模式串的匹配,而AC自动机适合多模式串的匹配。例如:在一篇文章中我们找一句话可以用KMP,找多句话适用于AC自动机。并且可以这么认为,KMP是AC自动机的特殊情况

实现AC自动机

算法思路
我们先是用模式串构建了trie树,以单词she, shr, say, her作为例子
在这里插入图片描述
这就是我们构建的trie树了。
在kmp算法中我们构建了一个nex数组(查询表),通过查询表,在我们每次失配的情况下快速移动模式串,从而避免了大量的不必要的比较。
nex[i]的含义以p[i]j结尾的后缀,能够匹配的从1开始非平凡前缀的最大坐标。在AC自动机中的含义也是同样的。
那么我们可以故技重施,为AC自动机也构建一个查询表,在AC自动机中叫失配指针。
同时,在KMP中,初始值nex[0] = nex[1] = 0, 另外如果我们需要求出nex[i],则需要用到nex[0]-nex[i-1],在AC自动机中,如果我们需要求出第i层点的nex值,则需要使用到i-1层(包括)前面的nex值。
由于每次向下求一层的nex,所以用bfs。
举个nex指针的例子:
在这里插入图片描述
红色代表nex指向,(其中只花了部分点的nex)
以下是构建kmp中nex数组大的代码











void getNex(char p[], int lenp) { 
    for(int i = 2, j = 0; i <= lenp; i++) { 
    while(j && p[i] != p[j+1]) j = nex[j]; if(p[i] == p[j+1] ) j++; nex[i] = j; } } 

类比代码

void bulid() { 
    int hh = 0, tt = -1; //第一层和第二层的nex值已经确定 //所以直接将第二层节点的孩子加入队列中就行 //ps:以26个英文字母为例 for(int i = 0; i <26; i++) if(tr[0][i]) q[++tt] = tr[0][i]; while(hh <= tt) { 
    int t = q[hh++]; for(int i = 0; i < 26; i++) { 
    int c = tr[t][i];//相当于kmp的i if(!c) continue; int j = nex[t]; while(j && !tr[j][i]) j = nex[j];//当j没有i字母的孩子 if(tr[j][i]) j = tr[j][i]; nex[c] = j; q[++tt] = c;//将c加入队列 } } } 

匹配过程

以下是kmp算法的匹配过程

 void kmp(char t[], char p[], int lent,int lenp) { 
    for(int i = 1, j = 0; i <= lent; i++) { 
    while(j && t[i] != p[j+1]) j = nex[j]; if(t[i] == p[j+1]) j++; if(j == lenp) { 
    printf("%d\n", i - lenp + 1);//匹配成功的位置 j = nex[j]; } } } 

同样类比于kmp的匹配过程,我们同样可以得出AC自动机的匹配过程

//tr中已经储存了需要查询的模式串 //str是文本串 //cnt记录每个点的单词数 //我们需要找到有多少个模式串在文本串中出现过 for(int i = 0, j = 0; str[i]; i++) { 
    int t = str[i] - 'a'; while(j && !tr[j][t]) j = nex[j]; if(tr[j][t]) j = tr[j][t]; int p = j; //while循环下面解释 while(p) { 
    res += cnt[p]; cnt[p] = 0; p = nex[p]; } } 

这段代码所匹配的是以str[i]为结尾的最长后缀,我们假设文本串为qwher,模式串为he, whe。那么当str[i]为e的时候,我们所匹配的是whe这个模式串,但是由于he是whe的后缀,he也应当出现在文本串中,所以我们需要加上whe的e的nex指针指向的点。

但由于while循环的出现,在比较坏的情况下我们难以接近线性复杂度,而是趋近于o(n^2)的复杂度, 因此我们需要对此优化。也就是优化成为trie图

如何优化成为trie图

消耗时间的部分主要是这一行代码

while(j && !tr[j][t]) j = nex[j]; 
void bulid() { 
    int hh = 0, tt = -1; //第一层和第二层的nex值已经确定 //所以直接将第二层节点的孩子加入队列中就行 for(int i = 0; i <26; i++) if(tr[0][i]) q[++tt] = tr[0][i]; while(hh <= tt) { 
    int t = q[hh++]; for(int i = 0; i < 26; i++) { 
    int p = tr[t][i]; if(!p) tr[t][i] = tr[nex[t]][i];//如果不存在这个儿子 else//如果存在这个儿子 { 
    nex[p] = tr[nex[t]][i]; q[++tt] = p; } } } } 

同时匹配过程也就可以直接去掉while循环

for(int i = 0, j = 0; str[i]; i++) { 
    int t = str[i] - 'a'; j = tr[j][t]; int p = j; while(p) { 
    res += cnt[p]; cnt[p] = 0; p = nex[p]; } } 
#include <bits/stdc++.h> using namespace std; const int M = 1e6 + 10; const int N = 128; int n; int tr[M][N], nex[M], cnt[M]; int q[M], hh , tt = -1; char str[M]; int idx, res; bool st[M]; void Insert() { 
    int p = 0; for(int i = 0; str[i]; i++) { 
    int t = str[i]; if(!tr[p][t]) tr[p][t] = ++idx; p = tr[p][t]; } cnt[p]++; } void init() { 
    memset(tr, 0, sizeof tr); memset(nex, 0, sizeof nex); memset(cnt, 0, sizeof cnt); idx = res = 0; } void bulid() { 
    int hh = 0, tt = -1; //第一层和第二层的nex值已经确定 //所以直接将第二层节点的孩子加入队列中就行 for(int i = 0; i < N; i++) if(tr[0][i]) q[++tt] = tr[0][i]; while(hh <= tt) { 
    int t = q[hh++]; for(int i = 0; i < N; i++) { 
    int p = tr[t][i]; if(!p) tr[t][i] = tr[nex[t]][i];//如果不存在这个儿子 else//如果存在这个儿子 { 
    nex[p] = tr[nex[t]][i]; q[++tt] = p; } } } } int main() { 
    init(); scanf("%d", &n); for(int i = 0; i < n; i++) { 
    scanf("%s", str); Insert(); } bulid(); scanf("%s", str); for(int i = 0, j = 0; str[i]; i++) { 
    int t = str[i]; j = tr[j][t]; int p = j; //在访问过一次p后,下一次就没有必要访问了,否则会重复计算,浪费时间。 while(p && st[p] == 0) { 
    st[p] = 1; res += cnt[p]; cnt[p] = 0; p = nex[p]; } } printf("%d\n", res); return 0; } 
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/232538.html原文链接:https://javaforall.net

(0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • android Activity的onPause()与onResume()[通俗易懂]

    android Activity的onPause()与onResume()[通俗易懂]官方文档地址:http://www.android-doc.com/training/basics/activity-lifecycle/pausing.html#Resume Pause和Resume一个Activity在一般的app使用中,前台的activity一般是会被视觉组件所遮住的,这就会导致activity的pause。举个例子,当一个半透明的activity打开的时候(就…

    2022年6月2日
    202
  • FlashFXP 注册码

    FlashFXP 注册码FlashFXP注册码——–FlashFXPRegistrationDataSTART——–FLASHFXPvACq2ssbvAAAAAC1W7cJKQTzmx77zmqJICvA7d3WnUtWNXdrp8YuERRFdIvXfOPbcpABkVix2aRTgg6afcIKFPxS72XYljdE9tgQD/2r+kmfVBngGM4Qc9p7…

    2022年7月26日
    10
  • PHPstrom 2021.2激活码破解方法[通俗易懂]

    PHPstrom 2021.2激活码破解方法,https://javaforall.net/100143.html。详细ieda激活码不妨到全栈程序员必看教程网一起来了解一下吧!

    2022年3月14日
    66
  • 关于c#中的dialogresult

    关于c#中的dialogresult
    在botton里面设置dialogresult为ok的时候,按下按钮窗口会自然关闭,这是由于窗口时模态显示的原因
    这种方式多用于设置对话框吧。。。
    但是更多时候必须判断对话框里里的输入是否有效或者其他一些判断
    所以不推荐奖button的dialogresult属性直接设置为ok
    而是动态用代码实现,但条件成立的时候写上
    this.DialogResult=DialogResult.ok;
    这样可以避免一些异常的捕捉和判断。。。

    2022年6月22日
    41
  • 区号秀数据库080308

    区号秀数据库080308今天突然接到一个同事的电话,区号秀竟然提示“未知”,发觉是数据库又旧了(07.11份的),150号段不支持,于是刚找到下面的最新版。转过来分享!区号秀2.10,塞班智能手机必备软件,稳定,省内存,省资

    2022年7月4日
    22
  • 2011年全国电子设计大赛综合测试题_全国大学生英语竞赛 C类

    2011年全国电子设计大赛综合测试题_全国大学生英语竞赛 C类系统方案总体设计方案本系统主要由电位器模块、直流减速电机模块、电源模块、电机驱动模块、单片机最小系统班组成。电位器与主控芯片STM32F407ZGT6相连,通过电位的测算实时向MCU发送摆杆的状态,MCU通过控制LM298N电机驱动模块来控制直流减速电机,进而控制摆杆的状态,并使用LCD显示相关参数。方案的比较与选择2.1传感器的选择方案一:采用三轴陀螺仪测量摆杆的偏转角度。当选用三轴陀螺仪检测摆杆的偏转角度时,虽然可以计算摆杆的偏转角度,但是传感器必须要固定在摆杆上,同时需与M…

    2022年8月18日
    6

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号