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

也许我写过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. Vue自定义组件以及组件通信的几种方式

    本帖子来源:小贤笔记 功能 组件 (Component) 是 Vue.js 最强大的功能之一.组件可以扩展 HTML 元素,封装可重用的代码.在较高层面上,组件是自定义元素,Vue.js 的编译器为它 ...

  2. N1考试必备词汇

    相次ぐ あいつぐ 淡い あわい 合間 あいま 渋い しぶい 相俟つ あいまつ 慌てよう あわてよう 明るい あかるい 安易過ぎる 明らか あきらか 用心 ようじん 悪事 あくじ 案の定 あんのじょう ...

  3. 直通BAT必考题系列:JVM性能调优的6大步骤,及关键调优参数详解

    JVM内存调优 对JVM内存的系统级的调优主要的目的是减少GC的频率和Full GC的次数. 1.Full GC 会对整个堆进行整理,包括Young.Tenured和Perm.Full GC因为需要对 ...

  4. time:时间就是生命

    golang中的time包是用来处理时间的. 1.时间的基本属性 package main import ( "fmt" "strings" "tim ...

  5. Centos 7安装部署zabbix 3.0LTS

    1.环境准备 OS:CentOS 7.2 64bit Zabbix版本:3.0.12 MySQL版本:5.6 注意:zabbix3.0相关要求 mysql5.0以上版本.apache1.3以上版本.p ...

  6. Linux: df du

    df :列出文件系统的整体磁盘使用量 du :评估文件系统的磁盘使用量(常用在推估目录所占容量) 1 df  :[-ahikHTm] 目录或文件名 选项或参数: -a:  列出所有的文件系统,包括系统 ...

  7. 【鸽】poj3311 Hie with the Pie[状压DP+Floyd]

    题解网上一搜一大坨的,不用复述了吧. 只是觉得网上dp方程没多大问题,但是状态的表示含义模糊.不同于正常哈密顿路径求解,状态表示应当改一下. 首先定义一次移动为从一个点经过若干个点到达另一个点,则$f ...

  8. 前端面试题-clearfix(清除浮动)

    一.浮动的概念 浮动的框可以向左或向右移动,直到它的外边缘碰到包含框或另一个浮动框的边框为止.由于浮动框不在文档的普通流中,所以文档的普通流中的块框表现得就像浮动框不存在一样. 二.浮动的影响 1. ...

  9. 更新protobuf

    sudo pip install --upgrade protobuf sudo apt-get install libprotobuf-dev protobuf-compiler这个不知道是什么问题 ...

  10. 用CSS如何实现单行图片与文字垂直居中

    图片样式为 以下为引用的内容:.style img{vertical-align:middle;.....} 如果STYLE中有其它如INPUT或其它内联元素可写成 以下为引用的内容:.style i ...