sshwy
2018-08-13 11:20:29
看dalao们AC自动机的Blog,大多数奆奆都会感性地说:
AC_automation = KMP+TRIE
<!--more-->
然而在作者重蹈覆辙辗转反侧n次后才明白,这东西说了等于没说。
建立一个AC自动机通常需要两个步骤:
然后就可以利用它进行多模式匹配了。
在讲构造以前,先来对比一下这里的
下面介绍构建
下面放一张GIF帮助大家理解:
对字典树{i,he,his,she,hers}构建
黄色结点表示当前的结点u,绿色结点表示已经BFS遍历完毕的结点,红/橙色的边表示
2号节点的
我们重点分析结点6的
找到6的父节点5,5的
所以跳到10的
所以
另外,在构建
const int N=1000;
struct AC_automaton{
int tr[N][26],cnt;//TRIE
int e[N];//标记这个结点是不是字符串结尾
int fail[N];//fail指针
void insert(char * s){}//插入模式串
void build(){}//构建fail指针
int query(char *t){}//匹配函数
};
AC_automation ac;
}
insert()
笔者不做分析,先来看build()
:
void build(){
queue<int>q;
memset(fail,0,sizeof(fail));
for(int i=0;i<26;i++)if(tr[0][i])q.push(tr[0][i]);//Q1
while(!q.empty()){
int k=q.front();q.pop();
for(int i=0;i<26;i++){
if(tr[k][i]){
fail[tr[k][i]]=tr[fail[k]][i];//Q2
q.push(tr[k][i]);
}
else tr[k][i]=tr[fail[k]][i];//Q3
}
}
}
首先声明了一个队列用于BFS,并清空
这里的字典树根节点为0,我们将根节点的子节点一一入队。
然后开始BFS:
这三个Questions构成了
Q2与Q3的代码是相辅相成的。
简单地来讲,我们将
而这个路径压缩的就是Q3的代码在做的事情之一。
我们将之前的GIF图改一下:
蓝色结点表示BFS遍历到的结点k,深蓝色、黑色的边表示执行完Q3代码连出的字典树的边。
可以发现,众多交错的黑色边将字典树变成了字典图。
图中省略了连向根节点的黑边(否则会更乱)。
我们重点分析一下结点5遍历时的情况:
显然,本来应该跳2次才能找到7号结点,但是我们通过10号结点的黑色边直接通过字母s找到了7号结点。
因此,Q2结合了Q3的代码,就能在
这就是build完成的两件事:构建
query()
:
int query(char *t){
int p=0,res=0;
for(int i=0;t[i];i++){
p=tr[p][t[i]-'a'];//Q
for(int j=p;j&&~e[j];j=fail[j])res+=e[j],e[j]=-1;
}
return res;
}
还记得刚才的字典图吗?事实上你并不是一直在往后跳,而是在图上穿梭跳动。比如,刚才的字典图:
我们从根节点开始尝试匹配ushersheishis
,那么p的变化将是:
红色结点表示p结点,粉色箭头表示p在字典图上的跳转,浅蓝色的边表示成功匹配的模式串,深蓝色的结点表示跳
其中的部分跳转,我们利用的就是新构建的字典图上的边,它也满足后缀相同(sher和her),所以自动跳转到下一个位置。
综上,
到此,你已经理解了整个AC自动机的内容。我们一句话总结AC自动机的运行原理: 构建字典图实现自动跳转,构建失配指针实现多模式匹配。
const int N=1000;
struct AC_automaton{
int tr[N][26],cnt;//TRIE
int e[N];//标记字符串结尾
int fail[N];//fail指针
void insert(char * s){//插入模式串
int p=0;
for(int i=0;s[i];i++){
int k=s[i]-'a';
if(!tr[p][k])tr[p][k]=++cnt;
p=tr[p][k];
}
e[p]++;
}
void build(){
queue<int>q;
memset(fail,0,sizeof(fail));
for(int i=0;i<26;i++)if(tr[0][i])q.push(tr[0][i]);
//首字符入队
//不直接将0入队是为了避免指向自己
while(!q.empty()){
int k=q.front();q.pop();//当前结点
for(int i=0;i<26;i++){
if(tr[k][i]){
fail[tr[k][i]]=tr[fail[k]][i];//构建当前的fail指针
q.push(tr[k][i]);//入队
}
else tr[k][i]=tr[fail[k]][i];
//匹配到空字符,则索引到父节点fail指针对应的字符,以供后续指针的构建
//类似并差集的路径压缩,把不存在的tr[k][i]全部指向tr[fail[k]][i]
//这句话在后面匹配主串的时候也能帮助跳转
}
}
}
int query(char *t){
int p=0,res=0;
for(int i=0;t[i];i++){
p=tr[p][t[i]-'a'];
for(int j=p;j&&~e[j];j=fail[j])res+=e[j],e[j]=-1;
}
return res;
}
};
AC_automation ac;