不知不觉这篇博客已经被我咕咕咕了一个月了。

也许我写过AC自动机的博客,但我也不知道我写没写过

前情回顾之\(kmp\)

\(kmp\)用来解决一个模式串匹配一个文本串的问题,其思路是设置失配指针,找与以当前字符的前一个字符结尾的后缀相同的最长的前缀的长度,失配时直接跳失配指针。复杂度\(O(m+n)\)

前情回顾之\(trie\)树

\(trie\)就是字典树。顾名思义,将单词放在树上。这里用到的\(trie\)每个节点有26条出边,代表26个字母。每个节点是一个字母,这样一条路径上所有节点的字母就可以组成一个单词。支持插入,删除,查重balabala……

关于前情回顾部分就不多说了qwq

什么是AC自动机鸭

现在有好多模式串以及一个超长的文本串,让你求每个模式串出现的次数。

对每个模式串都来一次\(kmp\)?想法不错,可惜\(T\)了。

传说中,有一个东西叫做AC自动机,是在\(trie\)上进行\(kmp\)的神奇的东西,可以解决上面那个问题。以及它并不能帮助你自动AC

好了我们来建个AC自动机叭

上面说过,AC自动机是在\(trie\)的基础上进行\(kmp\)。放进\(trie\)中的是所有的模式串。那如何在\(trie\)上处理\(nxt\)数组呢?这里类似\(kmp\),找与当前字符节点的父节点结尾的某一段后缀相同的最长的前缀所在的位置。(在kmp中因为只有一个模式串,所以前缀长度也就是位置,trie不一样)。它的父节点的失配指针一定指向有相同前缀的一个节点,所以我们只需要看父节点的失配指针指向的节点是否有与这个节点字符相同的儿子即可。如果有,这个儿子即是它的失配指针,没有就继续跳失配指针,如果一直没有,失配指针就是\(root\)。

来张图李姐李姐

模式串:ababb ababc bab



节点旁边的数字是节点编号,里面的字符就是这个节点的字符。其中0是root,规定0的所有出边都连向1,\(nxt[1]=0\)

首先,\(nxt[1]=0\)



遵循一个节点的失配指针是它父亲失配指针的对应的儿子的原则,发现1的两个儿子\(a\)和\(b\)的失配指针都指向1.



寻找节点3的失配指针,走它的父亲的失配指针,到1。发现1有对应的儿子8号节点。



由于失配指针依赖父节点的失配指针,是从上往下一层一层的来,所以我们现在找9号节点的失配指针,发现是2号节点。



诸如此类,最后\(nxt\)画完了是这个样子



其中6号节点的父亲的失配指针中没有对应的儿子,所以一直跳失配指针,直到跳到1,发现8号节点也是b。

代码:

queue <int> q;
void pre()
{
nxt[1]=0;
q.push(1);
while(!q.empty())
{
int u=q.front();
q.pop();
for(int i=0;i<26;i++)//用当前节点更新它的儿子
{
int v=trie[u][i];
if(!v)
trie[u][i]=trie[nxt[u]][i];//对之后的节点来说,相当于直接跳失配指针
else
{
q.push(v);
int qwq=nxt[u];
nxt[v]=trie[qwq][i];
}
}
}
}

统计答案

我们在\(trie\)上按照文本串的字符一直往下走,同时我们想统计文本串上每个字符对答案的贡献,也就是以该字符结尾,被包含的模式串的个数。根据\(nxt\)的找法,发现自己的失配指针所指的节点如果是某个模式串的结尾,那么就会对答案造成贡献。所以我们不妨一直跳失配指针来累加答案。

void getans()
{
int now=1,len=strlen(ms),v;
for(int i=0;i<len;i++)
{
int u=ms[i]-'a';
v=trie[now][u];
while(v)
{
if(en[v]==-1) break;//避免重复计算
ans+=en[v];//可能会出现重复的单词
en[v]=-1;
v=nxt[v];
}
now=trie[now][u];
}
}

这样最简单的AC自动机就Ok了,复杂度\(O((N+M)L)\),\(N\)是模式串个数,\(M\)是文本串长度,\(L\)是所有模式串的平均长度。

完整代码(luogu板子题):

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<vector>
#include<map>
#include<queue>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
inline ll read()
{
char ch=getchar();
ll x=0;bool f=0;
while(ch<'0'||ch>'9')
{
if(ch=='-') f=1;
ch=getchar();
}
while(ch>='0'&&ch<='9')
{
x=(x<<3)+(x<<1)+(ch^48);
ch=getchar();
}
return f?-x:x;
}
int n,trie[1000009][29],cnt=1,nxt[1000009],ans;
char ms[1000009];
int en[1000099];
void add()//建trie树
{
int len=strlen(ms);
int now=1;
for(int i=0;i<len;i++)
{
int ch=ms[i]-'a';
if(!trie[now][ch])
trie[now][ch]=++cnt;
now=trie[now][ch];
}
en[now]++;
}
queue <int> q;
void pre()
{
nxt[1]=0;
q.push(1);
while(!q.empty())
{
int u=q.front();
q.pop();
for(int i=0;i<26;i++)
{
int v=trie[u][i];
if(!v)
trie[u][i]=trie[nxt[u]][i];
else
{
q.push(v);
int qwq=nxt[u];
nxt[v]=trie[qwq][i];
}
}
}
}
void getans()
{
int now=1,len=strlen(ms),v;
for(int i=0;i<len;i++)
{
int u=ms[i]-'a';
v=trie[now][u];
while(v)
{
if(en[v]==-1) break;
ans+=en[v];
en[v]=-1;
v=nxt[v];
}
now=trie[now][u];
}
}
int main()
{
n=read();
for(int i=1;i<=n;i++)
{
scanf("%s",ms);
add();
}
for(int i=0;i<26;i++)
trie[0][i]=1;
pre();
scanf("%s",ms);
getans();
printf("%d\n",ans);
}

拓扑建图优化

现在有一个duliu出题人,让你求每个模式串出现了多少次。

显然我们可以用普通的AC自动机来不断跳失配指针来更新每个点的答案。

普通的AC自动机的复杂度是\(O(\)模式串长度\(\times\)文本串长度\()\)的,在不友好的数据面前容易被卡成AC自闭机,所以我们要想办法让它变成\(O(\)模式串长度\()\)的。

普通AC自动机的复杂度都浪费在哪里了呢?结合刚才那张图看一下



我们统计答案时,要不断暴力跳失配指针,这样,8号节点就被更新了多次。我们考虑一下有没有能一次更新完8号节点的答案的方法。在普通AC自动机更新答案的过程中,8号节点被3,10,5,6号节点更新,也就是说8号节点的答案由3,10,5,6号节点转移过来。那么我们可以想到用一个标记记录下答案,然后从底下往上更新每个点的答案,顺带更新标记。

这有没有很像拓扑?所以我们可以用拓扑图来实现,将\(nxt\)和\(trie\)上的边都看作是拓扑图里的边。

luogu AC自动机二次加强版的板子

#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<vector>
#include<queue>
#include<map>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
inline ull read()
{
char ch=getchar();
ull x=0;
bool f=0;
while(ch<'0'||ch>'9')
{
if(ch=='-') f=1;
ch=getchar();
}
while(ch>='0'&&ch<='9')
{
x=(x<<3)+(x<<1)+(ch^48);
ch=getchar();
}
return f?-x:x;
}
int n,trie[1000009][29],cnt=1,nxt[1000009];
string ms;
int en[1000099],t,ys[1000099],in[1000099];
int ans[1000009],qaq[1000009];
void add(int lz)
{
int len=ms.size();
int now=1;
for(int i=0;i<len;i++)
{
int ch=ms[i]-'a';
if(!trie[now][ch])
trie[now][ch]=++cnt;
now=trie[now][ch];
}
en[now]++;
if(en[now]==1)
ys[now]=lz;//记录当前单词结尾节点的出现的第一个模式串的编号
else qaq[lz]=ys[now],en[now]=1;//记录一个映射,因为题目中要求每个模式串的答案,对于重复出现的模式串,编号为i,ans[i]=ans[qaq[i]]
}
queue <int> q;
void pre()
{
nxt[1]=0;
q.push(1);
while(!q.empty())
{
int u=q.front();
q.pop();
for(int i=0;i<26;i++)
{
int v=trie[u][i];
if(!v)
trie[u][i]=trie[nxt[u]][i];
else
{
q.push(v);
int qwq=nxt[u];
nxt[v]=trie[qwq][i];
in[nxt[v]]++;
}
}
}
}
int an[1000099];
void getans()
{
int len=ms.size(),now=1;
for(int i=0;i<len;i++)
{
int c=ms[i]-'a';
now=trie[now][c];
an[now]++;//an就是一个标记
}
for(int i=1;i<=cnt;i++)
if(!in[i]) q.push(i);
while(!q.empty())
{
int u=q.front();
q.pop();
if(en[u])
ans[ys[u]]=an[u];
int v=nxt[u];
an[v]+=an[u];in[v]--;
if(!in[v]) q.push(v);
} }
int main()
{
n=read();
if(n==0) return 0;
for(int i=1;i<=n;i++)
{
cin>>ms;
add(i);
}
for(int i=0;i<26;i++)
trie[0][i]=1;
pre();
cin>>ms;
getans();
for(int i=1;i<=n;i++)
{
if(!ans[i]) ans[i]=ans[qaq[i]];
printf("%d\n",ans[i]);
}
}

AC自动机小记的更多相关文章

  1. 基于trie树做一个ac自动机

    基于trie树做一个ac自动机 #!/usr/bin/python # -*- coding: utf-8 -*- class Node: def __init__(self): self.value ...

  2. AC自动机-算法详解

    What's Aho-Corasick automaton? 一种多模式串匹配算法,该算法在1975年产生于贝尔实验室,是著名的多模式匹配算法之一. 简单的说,KMP用来在一篇文章中匹配一个模式串:但 ...

  3. python爬虫学习(11) —— 也写个AC自动机

    0. 写在前面 本文记录了一个AC自动机的诞生! 之前看过有人用C++写过AC自动机,也有用C#写的,还有一个用nodejs写的.. C# 逆袭--自制日刷千题的AC自动机攻克HDU OJ HDU 自 ...

  4. BZOJ 2434: [Noi2011]阿狸的打字机 [AC自动机 Fail树 树状数组 DFS序]

    2434: [Noi2011]阿狸的打字机 Time Limit: 10 Sec  Memory Limit: 256 MBSubmit: 2545  Solved: 1419[Submit][Sta ...

  5. BZOJ 3172: [Tjoi2013]单词 [AC自动机 Fail树]

    3172: [Tjoi2013]单词 Time Limit: 10 Sec  Memory Limit: 512 MBSubmit: 3198  Solved: 1532[Submit][Status ...

  6. BZOJ 1212: [HNOI2004]L语言 [AC自动机 DP]

    1212: [HNOI2004]L语言 Time Limit: 10 Sec  Memory Limit: 162 MBSubmit: 1367  Solved: 598[Submit][Status ...

  7. [AC自动机]【学习笔记】

    Keywords Search Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 131072/131072 K (Java/Others)To ...

  8. AC自动机 HDU 3065

    大概就是裸的AC自动机了 #include<stdio.h> #include<algorithm> #include<string.h> #include< ...

  9. AC自动机 HDU 2896

    n个字串 m个母串 字串在母串中出现几次 #include<stdio.h> #include<algorithm> #include<string.h> #inc ...

随机推荐

  1. 关于IDEA的application.properties读取乱码,以及显示乱码问题

    设置编码 如果设置之后还是不成功,就重启IDEA 再不行就删除application.properties重新编辑, 我采用的是注释掉要读取的中文部分,再下面再写一行

  2. Nginx(web服务器)与Tomcat(应用服务器)搭建集群

    Nginx作为互联网最常用的web服务器,高性能的HTTP和反向代理使它经常作为Tomcat集群的方案.Nginx官方只支持使用HTTP协议的集成,但是如果你想使用AJP协议集成,可以使用阿里开源的n ...

  3. 免费数学神器Mathpix发布移动版了,一起来写更快的公式

    目录 1. 按 2. 下载地址 3. 介绍和使用 3.1. 介绍 3.2. 实际使用体验 1. 按 本文介绍的Mathpix可用于将手写的公式通过拍照或截图转成LaTeX 表达式. 写博客.记笔记最麻 ...

  4. or/in/union与索引优化

    假设订单业务表结构为: order(oid, date, uid, status, money, time, …) 其中: oid,订单ID,主键 date,下单日期,有普通索引,管理后台经常按照da ...

  5. centos7搭建ntp时间同步服务器chrony服务

    centos7搭建ntp时间同步服务器chrony服务 前言: 在centos6的时候我们基本使用的是ntp服务用来做时间同步,但是在centos7后推荐是chrony作为时间同步器的服务端使用, ...

  6. yum源遇到的问题

    1.在配置CentOS的本地yum源时,所遇到的问题,本地yum设置失败 步骤: vim /etc/yum.repos.d/local.repo  设置本地源  可能会遇到本地源问题,注意使用tab键 ...

  7. 「工具」三分钟了解一款思维导图工具:XMind Zen

    一款非常实用的商业思维导图软件,融合艺术与创造力.致力于高效的可视化思维,强调软件的跨平台使用,帮助用户提高生产效率. 相关信息 · 操作系统:macOS / Windows / Linux · 官方 ...

  8. cyopen注释掉导入的动态函数

    cyopen注释掉导入的动态函数 cyopen注释掉导入的动态函数 cyopen注释掉导入的动态函数

  9. mysql每日数据统计

    select a.click_date from ( SELECT curdate() as click_date union all day) as click_date union all day ...

  10. [LeetCode 92] Reverse Linked List II 翻转单链表II

    对于链表的问题,根据以往的经验一般都是要建一个dummy node,连上原链表的头结点,这样的话就算头结点变动了,我们还可以通过dummy->next来获得新链表的头结点.这道题的要求是只通过一 ...