一直听说 A C AC AC自动机是一个很难很难的算法,而且它不在 N O I P NOIP NOIP提高组范围内(这才是关键),所以我一直没去学。
最近被一些字符串题坑得太惨,于是下定决心去学 A C AC AC自动机。
A C AC AC自动机是一个著名的多模字符串匹配算法,建立在** K M P KMP KMP算法和 T r i e Trie Trie字典树**的基础之上。
L i n k Link Link
K M P KMP KMP算法详见博客从暴力匹配到KMP算法
T r i e Trie Trie字典树详见博客Trie:字典树
其实,它的本质就相当于在一棵 T r i e Trie Trie上跑 K M P KMP KMP,真是一个十分强势的算法。
不得不说,在 A C AC AC自动机的实现中, T r i e Trie Trie起到了很大的作用:因为我们用它存下了每一个用来与文本串匹配的模式串。
我们可以新建一棵 T r i e Trie Trie如下:
struct Trie
{
int Son[26],sum,Next;//Son记录当前节点的儿子的位置,sum记录当前节点包含的字符串个数,Next记录失配指针
}node[N+5];
然后将每一个模式串插入 T r i e Trie Trie中,就完成一开始的存储部分了:
inline void Insert(string s)//Trie的插入真的十分简洁
{
register int i;int x=rt;//x记录当前节点
for(i=0;i<s.length();++i)
{
int p=s[i]-'a';//p记录下一个节点的编号
if(!node[x].Son[p]) node[x].Son[p]=++tot;//如果下一个节点不存在,就新建一个节点
x=node[x].Son[p];//将x更新为下一个节点
}
++node[x].sum;//将最终到达的节点所包含字符串的个数加1
}
A C AC AC自动机( K M P KMP KMP算法)的精髓就在于失配指针 N e x t Next Next(许多人把 A C AC AC自动机的失配指针称为 f a i l fail fail,不过,由于我习惯把预处理出 K M P KMP KMP中的 N e x t Next Next数组的函数称为 G e t N e x t ( ) GetNext() GetNext(),换成 G e t F a i l ( ) GetFail() GetFail()恐怕不太吉利…因此我依然用 N e x t Next Next来表示失配指针)。
与 K M P KMP KMP中的失配指针有点区别, A C AC AC自动机中的失配指针指向的是当前匹配到的字符串的最长后缀。
我们可以写一个函数 G e t N e x t ( ) GetNext() GetNext()来求出失配指针。
记得我在有关 K M P KMP KMP的一篇博客中提到过,求 N e x t Next Next数组的过程就是一个 K M P KMP KMP的过程,不得不说, A C AC AC自动机也是类似的。
不过,求失配指针的过程有点像一个 B F S BFS BFS,我们可以用一个队列来存储访问到的字符串,然后每次都求出队首的一个字符串(这样可以保证每次取出的字符串的长度是递增的),求出它的失配指针。
代码如下:
inline void GetNext()//求出失配指针,类似于广搜
{
register int i,k;q.push(rt);//初始化队列
while(!q.empty())//只要队列中还有元素
{
k=q.front(),q.pop();//取出队首的元素
for(i=0;i<26;++i)//枚举这个元素的每一个子节点
{
if(k^rt)//如果当前的元素不是根节点
{
if(!node[k].Son[i]) node[k].Son[i]=node[node[k].Next].Son[i];//如果当前节点这个儿子不存在,就将当前节点的失配指针的儿子作为当前节点的儿子
else node[node[k].Son[i]].Next=node[node[k].Next].Son[i],q.push(node[k].Son[i]);//如果当前节点有这个儿子,就将当前节点的儿子的失配指针指向当前节点的失配指针的这个儿子,并将当前节点加入队列
}
else//如果当前元素是根节点就特殊处理
{
if(!node[k].Son[i]) node[k].Son[i]=rt;
else node[node[k].Son[i]].Next=rt,q.push(node[k].Son[i]);
}
}
}
}
好了,讲完了失配指针, A C AC AC自动机的核心代码应该就很简单了吧。
这里以洛谷上一道简单的板子题为例,来贴一份代码:
inline void AC_Automation()//AC自动机的核心代码
{
register int i,j,x=rt,len=st.length();//x记录当前到达节点
for(GetNext(),i=0;i<len;++i)//枚举文本串上的每一个字符
{
if(!(x=node[x].Son[st[i]-97])) {x=rt;continue;}
int p=x;//用p来记录当前能匹配到的字符
while(p^rt)//只要p没有指向根
{
if(node[x].Cnt>=0) ans+=node[x].Cnt,node[x].Cnt=-1;//如果当前节点未被访问过,就更新匹配成功的字符串个数,并标记当前节点为已访问
else break;//否则退出循环,因为如果当前节点访问过了,那么当前节点失配指针指向的位置肯定也访问过了
p=node[p].Next;//更新当前节点为当前节点的失配指针
}
}
}
毕竟, A C AC AC自动机的题目不可能直接出裸题让你做字符串匹配的。
通常都只是一些小应用:
【洛谷3796】【模板】AC自动机(加强版)
【BZOJ4327】[JSOI2012] 玄武密码
【BZOJ3940】[USACO2015 Feb]Censoring
【BZOJ3172】[TJOI2013]单词
L i n k Link Link
【洛谷3796】【模板】AC自动机(加强版) 的题解详见博客【洛谷3796】【模板】AC自动机(加强版)
【BZOJ4327】[JSOI2012] 玄武密码 的题解详见博客【BZOJ4327】[JSOI2012] 玄武密码(AC自动机的小应用)
【BZOJ3940】[Usaco2015 Feb]Censoring 的题解详见博客【BZOJ3940】[USACO2015 Feb]Censoring(AC自动机的小应用)
【BZOJ3172】[TJOI2013]单词 的题解详见博客【BZOJ3172】[TJOI2013]单词(AC自动机的小应用)