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)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • 浏览器报错400系列总结「建议收藏」

    浏览器报错400系列总结「建议收藏」 

    2022年5月18日
    50
  • 数据库连接池的理解和使用方法_为什么要使用数据库连接池

    数据库连接池的理解和使用方法_为什么要使用数据库连接池一、什么是数据库连接池?官方:数据库连接池(Connectionpooling)是程序启动时建立足够的数据库连接,并将这些连接组成一个连接池,由程序动态地对池中的连接进行申请,使用,释放。个人理解:创建数据库连接是一个很耗时的操作,也容易对数据库造成安全隐患。所以,在程序初始化的时候,集中创建多个数据库连接,并把他们集中管理,供程序使用,可以保证较快的数据库读写速度,还更加安全可靠。二…

    2022年9月17日
    4
  • 思科 计算机网络 期末考试答案

    思科 计算机网络 期末考试答案1.以下哪个域名是顶级域的一个示例?A.root.cisco.comB.www.cisco.comC.cisco.comD…com2.第2层数据封装有哪三个主要功能?(请选择三项。)A.将位组定界为帧B.通过冲突检测方法纠正错误C.在介质中放置和删除帧D.将位转换为数据信号E.通过CRC计算检测错误F.数据链路层寻址G.使用端口号控制会话3.管理员在发出ping命令之后在交换机上使用Ctrl-Shift-6键组合。使用这些按键有什么用途?A.允许用户完成命令B.中

    2022年7月23日
    15
  • win10 ipconfig flushdns 清除DNS缓存,修复上网问题

    win10 ipconfig flushdns 清除DNS缓存,修复上网问题win10ipconfigflushdns清除DNS缓存,修复上网问题
    一、使用ipconfig/flushdns命令刷新DNS解析缓存
    1、右键点击系统桌面左下角的【开始】,在开始的右键

    2022年7月2日
    29
  • 汇编学习 step by step[通俗易懂]

    汇编学习 step by step[通俗易懂]转自:http://hi.baidu.com/hkbyest16位汇编对于一个汇编初学者,首先必看的就是王爽老师的这本《汇编语言》,虽然它不是很完整,虽然它有一些错漏,虽然它需要一些前置知识(详见书籍前言部分,前言一定要仔细看!),但是王爽老师独特的教学理念构造了这本循序渐进的书,我们从中可以抛开对汇编语言的畏惧心态,一步一步的深入进去,更可喜的是在这本书里我们可以学到宝贵的底层编程意识和

    2022年10月13日
    1
  • 创意人像海报故障艺术海报教程故障艺术海报怎么做

    创意人像海报故障艺术海报教程故障艺术海报怎么做制作之前我们可以参考一下真实的电视故障效果 然后通过 PS 功能来实现 首先要分析一下图片合适不合适 图片太小会不会看不出来效果等各种问题 类似这种效果最好上半身人像为好 这样做出来的效果会更加的明显 文章来源 http www mo yu com

    2025年11月3日
    3

发表回复

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

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