学了AC自动机,自己把理解做成了ppt
接下来把思路粘过来好了:
•首先,对于模式串和文本串的匹配,我们是用的kmp算法。
•Kmp在传统O(mn)算法上优化至O(m+n).
•但是一个串要和多个串匹配呢?
•N个kmp?
•
•超时TLE
•
•我们引入AC自动机
•需要知识:kmp匹配原理,字典树知识。
•复习:kmp
•Kmp思想大致是现将文本串自我匹配得到一个next数组(失配指针)
•当文本串第i位和模式串第j位逐位比较时,一旦失配,我们就不必返回初始i+1位和j=1位进行比较,而是利用失配指针,文本串指针指向next[i]继续匹配(next[i]=-1时表示后面找不到这样的前缀,需要j从1开始匹配)
•大致思想是,每次比较到某一位时,发现失配了,但是文本前面一部分和模式串前面一部分已经匹配上了,如果我们提前知道后面哪个位置开始的也是这么多长度的串与之相等(比如abcdrabce和abce匹配,前3个abc已经匹配了但当前失配,后面也有一个abc开始的串,利用计算好的next数组我们可以把第一个串的指针移动到后面的abc的后面再进行比较)
•理解字典树原理:(不需要知道太深)
•像这样的,root为空作为字典树入口,从它开始的任意一条路径(到叶子节点)上的字母连起来都是一个串(模式串),有了这样的一棵树,我们就可以在变形之后利用它在文本串中匹配。
•由上,观察下图可以知道这棵树是由abcd,abd,bcd,efg,hi组成的。
•不难想到,对于前面有某一相同前缀的串可以共用一些点。
•那么每次读入一个模式串,我们就把它插入这棵字典树。
•Struct node
•{
• node *fail;//失配指针,等会会用到
• node *next[26];//字母是26个,为了操作方便,我们对每个点建立26个子节点。
• int cnt;//以这个点为结尾字母的单词的个数;
•/*有可能出现这种情况 abc,abcd 那么我们需要在c的位置cnt++,d的位置cnt++ */
•Node(){fail=NULL;cnt=0;for(int i=0;i<26;i++)next[i]=NULL}//新节点
•}
•Node *root;
•//之后调用new node 貌似不需要内存池了。
•然后建树
//插入代码,比较简单:
void insert(char *s)
{
node *u=root;
int len=strlen(s);
int tmp;//
for(int i=0;i<len;i++)
{
tmp=s[i]-'a';
if(u->next[tmp]==NULL)u->next[tmp]=new node();
u=u->next[tmp];
}
u->cnt++;
}
•显然,只是插入这棵树,现在进行匹配也无从下手。
•暴力的话又会回到朴素算法。
•类似于kmp的,我们也需要构造next失配指针。
•我们利用bfs进行高效的构造。
•每次取出队首元素,把它的所有子节点扫一遍,扫的过程中对于每一个元素,我们从当前节点的失配指针开始往上找,直到找到有个点的next[i]和当前i相等,就把i和那个点的next[i]连一条边,即失配指针,然后把这个子节点压入队列。
•为什么这么做是对的,我提出我的理解(不一定是对的)
•对于每一层,我们总是从最左边的开始找,因为失配指针连接的两个点有公共前缀,也就保证了当前点u和它的子节点u->son[i]需要找到u的某个失配点p(p一定等于u),满足p->son[i]=u->son[i],那么u->son[i]的失配指针就是p->son[i].具体的流程画在下一页
•
•(图有点丑)
•现在假设我们前面已经处理好了,当前来到b(蓝色)这个点。
•我们需要找到它的失配指针。
•从它的父节点a开始,a的失配指针为root,root有一个儿子是b(黄色),那么b的失配指针就是root这个儿子了。
•------------------
•同样的,找左边的c的失配点,b的fail->root的b,root的b有个c(橙色)
•剩下的以此类推
•练习:
•模拟上述方案,找出所有失配指针并连接起来。
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
•看一下,连起来是不是这样:
•
•
•
•
•
•
•
•完成这样操作的函数
Void build_ac()
{
queue<node *>q;
q.push(root);
whlie(!q.empty())//宽搜队列
{
node *u=q.front();q.pop();
for(int i=0;i<26;i++)
{
node *son=u->next[i]; //对于每一个儿子
if(son!=NULL) //如果为空就不管了
{
if(u==root)son->fail=root;//根的情况,特判
else{
node *tmp=u->fail; //我们要匹配子节点的fail,子节点的父亲就是u;
while(tmp!=NULL)//不断找u的fail
{
if(tmp->next[i]!=NULL){son->fail=tmp->next[i];break;}//保证最先找到能匹配的点。
tmp=tmp->fail;
}
If(tmp==NULL)son->fail=root;//不能匹配,回到根
}
q.push(son);//每个节点进队一次出队一次
}
}
}
}
•处理之后,失配指针得到了,就下来就轻松了。
•令需要匹配的文本串为p
•那么 p[] i∈[1,m]
•M=strlen(p);
•For(int i=1;i<=m;i++)
•对于当前位,我们需要知道它能否匹配。
•这样考虑,我们看当前点u的下一位,对于s[i]-’a’这一位存不存在,存在的话就可以往下走,不存在就跳到失配指针那里继续比较。
•于是我们会有两种结果:
•①可以匹配,往下走;
•②不能匹配,回到了root,重新来。
•同时,把这个能匹配到的点以及所有失配指针上的点的cnt加起来(cnt是以当前字母为结尾的单词个数),并且标记一下,下次就不在计算了。
//代码:
Int query()
{
node *u=root;node *tmp;
int len=strlen(s);
int ans=0;
for(int i=0;i<len;i++)
{
int id=s[i]-’a’;//需要匹配的下一位
while(u->next[id]==NULL&&u!=root)u=u->fail;//如果失配需要一直跳失配指针,最后悔出现两种可能,如上页
u=u->next[i];
if(u==NULL)u=root;//到根且失配,返回根
tmp=u; //从u找所有失配指针(都是以u结尾,前缀相同)累加所有以它结尾的单词个数
while(tmp!=root&&tmp->cnt!=-1)//-1做标记,加了一次后不再加,且如果当前已经被计算,那么根据bfs求失配指针的性质可知后面的失配指针指向的位置一定也被计算了
{
ans+=tmp->cnt;
tmp->cnt=-1;
tmp=tmp->fail;
}
}
return ans;
}
•类似于kmp,ac自动机工作核心就是失配指针,整个程序的重点是如何构造失配指针,请务必先理解原理(至少要有大概印象,再去翻译理解代码)
•
•同时,我的入门题hdu 2222
•Or 非vjudge
推荐习题:hdu 2222,3065,2896.