学习ac自动机需要先会kmp和trie:kmp | trie
终于入门了,看了一天,现在整理一下。
算法简介:
AC自动机主要用于解决多模式串的匹配问题。
如hdu2222:
给定N(N <= 10000)个长度不大于50的模式串,再给定一个长度为L(L <= 106)目标串,求目标串出现了多少个模式串。
模式串: she he say shr her 目标串:yasherhs 目标串中出现的模式串一共有3个,分别为 she he her
算法步骤:
1.先用模式串建一个字典树
2.对字典序中的节点构建失配指针,构成trie图
3.用目标串进行查询
构建字典树
Trie 节点的结构体
struct Trie { Trie *next[maxn]; Trie *fail; int cnt;//结尾标记,统计以该节点结尾的单词个数 int v;//前缀标记,统计以该节点前的串为前缀的单词个数 Trie() { fail=NULL; cnt=0; v=0; for(int i=0;i<maxn;i++) next[i]=NULL; } }
Trie的构建不多说了,详见字典树
void creatTrie(Trie *root,char *str) { Trie *p=root; p=root; for(int i=0;str[i];i++) { int id=charToNum(str[i]); if(id==-1) { puts("error"); return; } if(p->next[id]==NULL) p->next[id]=new Trie(); p=p->next[id]; p->v++; } p->cnt++; }
失配指针
失配指针的含义
一个模式串的某个字符匹配失败的时候,就跳到它的失败指针上继续匹配,重复上述操作,直到这个字符匹配成功,所以失败指针一定满足一个性质,它指向的一定是某个串的前缀,并且这个前缀是当前结点所在前缀的后缀,而且一定是最长后缀。仔细理解一下这句话,首先,一定是某个串的前缀,这是显然的,因为trie树本来就是前缀树,它的任意一个结点都是某个模式串的前缀;然后再来看后面一句话,为了让当前字符能够找到匹配,那么当前结点的某个后缀必须要和某个模式串的前缀相匹配,这个性质就和KMP的next数组不谋而合了。
如何利用BFS求出所有结点的失败指针
1) 对于根结点root的失败指针,我们将它直接指向NULL,对于根结点下所有的子结点,失败指针一定是指向root的,因为当一个字符都不能匹配的时候,自然也就不存在更短的能够与之匹配的前缀了;
2) 将求完失败指针的结点插入队列中;
3) 每次弹出一个结点now,询问它的每个字符对应的子结点,为了阐述方便,我们将now的i号子结点记为now->next[i]:
a) 如果now->next[i]为NULL,那么将now->next[i]指向now的失败指针的i号子结点, 即 now->next[i] = now->fail->next[i];
b) 如果now->next[i]不等于NULL,则需要构造now->next[i]的失败指针,由于a)的操作,我们知道now的失败指针一定存在一个i号子结点,即now->fail->next[i],那么我们将now->next[i]的失败指针指向它,即now->next[i]->fail = now->fail->next[i];
4) 重复2)的操作直到队列为空;
void build_ac(Trie *root) { Trie *p,*tmp; int head=0,tail=0; q[tail++]=root; while(head!=tail) { p=q[head++]; for(int i=0;i<maxn;i++) { tmp=(p==root)?root:p->fail->next[i]; if(p->next[i]==NULL){ p->next[i]=tmp; } else{ p->next[i]->fail=tmp; q[tail++]=p->next[i]; } } } }
目标串匹配
对目标串进行匹配的时候,同样需要扫描目标字符串。由于trie图已经创建完毕,每个结点读入一个字符的时候都能够进入到下一个状态,所以我们只需要根据目标串给定的字符进行遍历,然后每次检查当前的结点是否是结尾结点,当然还需要检查p的失败指针指向的结点...累加所有的cnt和即为模式串的个数。
int query_ac(Trie *root,char *str) { int cnt=0; Trie *p=root,*tmp=NULL; for(int i=0;str[i];i++) { int id=charToNum(str[i]); if(id==-1) { p=root; continue; } p=p->next[id]; tmp=p; while(tmp!=root&&tmp->cnt!=-1) { if(tmp->cnt) { cnt+=tmp->cnt; tmp->cnt=-1; } tmp=tmp->fail; } } return cnt; }
hdu2222
1.构建字典树
2.构造失败指针
用BFS来构造失败指针,与KMP算法相似的思想。
首先,root入队,第1次循环时处理与root相连的字符,也就是各个单词的第一个字符h和s,因为第一个字符不匹配需要重新匹配,所以第一个字符都指向root(root是Trie入口,没有实际含义)失败指针的指向对应下图中的(1),(2)两条虚线;
第2次进入循环后,从队列中先弹出h,接下来p指向h节点的fail指针指向的节点,也就是root;p=p->next[i], 则把节点e的fail指针指向root表示没有匹配序列,对应图-2中的(3),然后节点e进入队列;
第3次循环时,弹出的第一个节点a的操作与上一步操作的节点e相同,把a的fail指针指向root,对应图-2中的(4),并入队;
第4次进入循环时,弹出节点h(图中左边那个),这时操作略有不同。由于p->next[i]!=NULL(root有h这个儿子节点,图中右边那个),这样便把左边那个h节点的失败指针指向右边那个root的儿子节点h,对应图-2中的(5),然后h入队。以此类推:在循环结束后,所有的失败指针就是图-2中的这种形式。
模式串: she he say shr her 目标串:yasherhs 目标串中出现的模式串一共有3个,分别为 she he her
3.扫描
构造好Trie和失败指针后,我们就可以对主串进行扫描了。这个过程和KMP算法很类似,但是也有一定的区别,主要是因为AC自动机处理的是多串模式,需要防止遗漏某个单词,所以引入temp指针。
匹配过程分两种情况:(1)当前字符匹配,表示从当前节点沿着树边有一条路径可以到达目标字符,此时只需沿该路径走向下一个节点继续匹配即可,目标字符串指针移向下个字符继续匹配;(2)当前字符不匹配,则去当前节点失败指针所指向的字符继续匹配,匹配过程随着指针指向root结束。重复这2个过程中的任意一个,直到模式串走到结尾为止。
对照上图,看一下模式匹配这个详细的流程,其中模式串为yasherhs。对于i=0,1。Trie中没有对应的路径,故不做任何操作;i=2,3,4时,指针p走到左下节点e。因为节点e的count信息为1,所以cnt+1,并且讲节点e的count值设置为-1,表示改单词已经出现过了,防止重复计数,最后temp指向e节点的失败指针所指向的节点继续查找,以此类推,最后temp指向root,退出while循环,这个过程中count增加了2。表示找到了2个单词she和he。当i=5时,程序进入第5行,p指向其失败指针的节点,也就是右边那个e节点,随后在第6行指向r节点,r节点的count值为1,从而count+1,循环直到temp指向root为止。最后i=6,7时,找不到任何匹配,匹配过程结束。
注意:AC自动机建立的过程改变了字典序原有的next[],所以不能再用字典序中的方法查询和释放内存
动态写法:
#include<cstdio> #include<cstring> #include<string> #include<iostream> #include<algorithm> using namespace std; #define maxn 26 #define N 500010 //定义队列大小 struct Trie { Trie *next[maxn]; Trie *fail; int cnt;//结尾标记,统计以该节点结尾的单词个数 int v;//前缀标记,统计以该节点前的串为前缀的单词个数 Trie() { fail=NULL; cnt=0; v=0; for(int i=0;i<maxn;i++) next[i]=NULL; } }*q[N]; int charToNum(char s) { if(s>='a'&&s<='z')return s-'a'; return -1; } void creatTrie(Trie *root,char *str) { Trie *p=root; p=root; for(int i=0;str[i];i++) { int id=charToNum(str[i]); if(id==-1) { puts("error"); return; } if(p->next[id]==NULL) p->next[id]=new Trie(); p=p->next[id]; p->v++; } p->cnt++; } void build_ac(Trie *root) { Trie *p,*tmp; int head=0,tail=0; q[tail++]=root; while(head!=tail) { p=q[head++]; for(int i=0;i<maxn;i++) { tmp=(p==root)?root:p->fail->next[i]; if(p->next[i]==NULL){ p->next[i]=tmp; } else{ p->next[i]->fail=tmp; q[tail++]=p->next[i]; } } } } int query_ac(Trie *root,char *str) { int cnt=0; Trie *p=root,*tmp=NULL; for(int i=0;str[i];i++) { int id=charToNum(str[i]); if(id==-1) { p=root; continue; } p=p->next[id]; tmp=p; while(tmp!=root&&tmp->cnt!=-1) { if(tmp->cnt) { cnt+=tmp->cnt; tmp->cnt=-1; } tmp=tmp->fail; } } return cnt; } char qu[1000010]; int main() { int T,n; scanf("%d",&T); char str[50]; while(T--) { Trie *root=new Trie(); scanf("%d",&n); while(n--) { scanf("%s",&str); creatTrie(root,str); } build_ac(root); scanf("%s",qu); printf("%d\n",query_ac(root,qu)); // Delete(root); AC自动机不能这么释放内存 } return 0; }
静态写法:
注意姿势:
query里面vis的作用 可以大大降低时间复杂度,这一步很关键。 是600+ms 优化到200+ms的关键
#include<cstdio> #include<cstring> #include<string> #include<queue> #include<iostream> #include<algorithm> using namespace std; #define maxn 26 #define N 500050 //定义队列大小,单词个数*长度 struct Trie { int next[N][maxn]; int fail[N];//失配指针 int cnt[N];//结尾标记,统计以该节点结尾的单词个数 int v[N];//前缀标记,统计以该节点前的串为前缀的单词个数 int q[N];//队列缓存 bool vis[N]; int root,L; void inti() { L=0; root=newnode(); } int newnode() { cnt[L]=0; v[L]=0; for(int i=0;i<maxn;i++) { next[L][i]=-1;//-1表示空节点 } v[L]=0; cnt[L++]=0; return L-1; } int charToNum(char s) { if(s>='a'&&s<='z')return s-'a'; return -1; } void insert(char str[]) { int now=root; for(int i=0;str[i];i++) { int id=charToNum(str[i]); if(id==-1) { puts("error"); return; } if(next[now][id]==-1) next[now][id]=newnode(); now=next[now][id]; v[now]++; } cnt[now]++; } void build() { int head=0,tail=0; int now,tmp; q[tail++]=root; while(head!=tail) { now=q[head++]; for(int i=0;i<maxn;i++) { tmp=(now==root)?root:next[fail[now]][i]; if(next[now][i]==-1) { next[now][i]=tmp; } else { fail[next[now][i]]=tmp; q[tail++]=next[now][i]; } } } } int query(char str[]) { memset(vis,false,sizeof(vis)); int ans=0; int now=root,tmp; for(int i=0;str[i];i++) { int id=charToNum(str[i]); if(id==-1) { now=root; continue; } now=next[now][id]; tmp=now; while(tmp!=root) { if(vis[tmp])break; vis[tmp]=true; ans+=cnt[tmp]; tmp=fail[tmp]; } } return ans; } }; char qu[1000010]; Trie ac; int main() { int T; scanf("%d",&T); while(T--) { int n; scanf("%d",&n); char str[100]; ac.inti(); while(n--) { scanf("%s",str); ac.insert(str); } ac.build(); scanf("%s",qu); printf("%d\n",ac.query(qu)); } return 0; }
参考:
zoj 3545 AC自动机+状态dp http://blog.csdn.net/Guard_Mine/article/details/45151769
http://www.cppblog.com/menjitianya/archive/2014/07/10/207604.html
http://blog.csdn.net/liuqiyao_01/article/details/8798241
http://blog.csdn.net/niushuai666/article/details/7002823
http://blog.csdn.net/niushuai666/article/details/7002736