以前我觉得AC自动机很高大尚,然而并不是这样的,其实很简单。
我们首先来复习一下kmp和Trie。
KMP
一种由Knuth(D.E.Knuth)、Morris(J.H.Morris)和Pratt(V.R.Pratt)三人设计的线性时间字符串匹配算法。这个算法不用计算变迁函数δ,匹配时间为Θ(n),只用到辅助函数π[1,m],它是在Θ(m)时间内,根据模式预先计算出来的。数组π使得我们可以按需要,“现场”有效的计算(在平摊意义上来说)变迁函数δ。粗略地说,对任意状态q=0,1,…,m和任意字符a∈Σ,π[q]的值包含了与a无关但在计算δ(q,a)时需要的信息。由于数组π只有m个元素,而δ有Θ(m∣Σ∣)个值,所以通过预先计算π而不是δ,使得时间减少了一个Σ因子。
KMP实现
计算出一个next数组,nexti是满足S(即原串)的长度为
然后匹配时就利用next数组不断寻求第一个可行解就好了。
代码:
fo(i,1,n){
while(len&&s[len+1]!=s[i])len=next[len];
if (s[len+1]==s[i])len++;
next[i]=len;
}
fo(i,1,n){
while(ans&&s[ans+1]!=p[i])ans=next[ans];
if (s[ans+1]==p[i])ans++;
cout<<ans<<endl;
}
Trie
在计算机科学中,Trie,又称前缀树或字典树,是一种有序树,用于保存关联数组,其中的键通常是字符串。与二叉查找树不同,键不是直接保存在节点中,而是由节点在树中的位置决定。一个节点的所有子孙都有相同的前缀,也就是这个节点对应的字符串,而根节点对应空字符串。一般情况下,不是所有的节点都有对应的值,只有叶子节点和部分内部节点所对应的键才有相关的值。
Trie实现
每次插入一个串S,从根节点开始向下,从头开始顺序插入,没有节点就新增再走过去,有就直接走。
代码:
fo(i,1,len){
if(!a[now].to[s[i]])a[now].to[s[i]]=++tot;
now=a[now].to[s[i]];
}
复习了前面两个关于字符串的算法,我们可以开始讲AC自动机了。
AC自动机,实际上就是KMP和Trie的合体版。
KMP是单模式匹配,而AC自动机是多模式匹配。
主要思路
- 我们把所有的模式串插入到Trie中。
- 我们定义一个
fail 数组,与KMP的next类似,faili指向一个节点,使得从根节点到faili的字符串是根节点到i的后缀,且长度最长。 - 我们可以用类似KMP的方法用BFS求出
fail 数组。- 最后就可以进行匹配了,从根节点开始走,如果匹配不了就跳到当前点的fail,知道可以匹配,然后就向下走。
模版:
#include <iostream> using namespace std; struct node { int next[26]; int fail; int count; void init() { memset(next, -1, sizeof(next)); fail = 0; count = 0; } }s[500005]; int sind; char str[55]; char des[1000005]; int q[500005], qin, qout; void cas_init() { s[0].init(); sind = 1; } void ins() { int len = strlen(str); int i, j, ind; for(i = ind = 0; i < len; i++) { j = str[i] - 'a'; if(s[ind].next[j] == -1) { s[sind].init(); s[ind].next[j] = sind++; } ind = s[ind].next[j]; } s[ind].count++; } void make_fail() { qin = qout = 0; int i, ind, ind_f; for(i = 0; i < 26; i++) { if(s[0].next[i] != -1) { q[qin++] = s[0].next[i]; } } while(qin != qout) { ind = q[qout++]; for(i = 0; i < 26; i++) { if(s[ind].next[i] != -1) { q[qin++] = s[ind].next[i]; ind_f = s[ind].fail; while(ind_f > 0 && s[ind_f].next[i] == -1) ind_f = s[ind_f].fail; if(s[ind_f].next[i] != -1) ind_f = s[ind_f].next[i]; s[s[ind].next[i]].fail = ind_f; } } } } int fd() { int ct = 0; int di, i, ind, p; int len = strlen(des); for(di = ind = 0; di < len; di++) { i = des[di] - 'a'; while(ind > 0 && s[ind].next[i] == -1) ind = s[ind].fail; if(s[ind].next[i] != -1) { ind = s[ind].next[i]; p = ind; while(p > 0 && s[p].count != -1) { ct += s[p].count; s[p].count = -1; p = s[p].fail; } } } return ct; } int main() { int cas, n; scanf("%d", &cas); while(cas-- && scanf("%d", &n)) { gets(str); cas_init(); while(n-- && gets(str)) ins(); make_fail(); gets(des); printf("%d\n", fd()); } return 0; } #include <iostream> using namespace std; struct node { int next[26];//每一个节点可以扩展到的字母 int fail;//每一个节点的失配指针 int count;//记录每一个可以构成单词的字符串距根节点的深度 void init()//构造 { memset(next, -1, sizeof(next));//初始化next为-1,即不与任何值相连 fail = 0;//失配指针为空 count = 0;//一开始没有单词赋为0 } }s[500005]; int sind;//记录节点的编号 char str[55];//模板串,”单词“ char des[1000005];//”文章“ int q[500005], qin, qout;//队列 void cas_init()//在整个程序前构造root { s[0].init();//初始化头结点 sind = 1;//当前有一个节点 } void ins()//向书中插入字母 { int len = strlen(str);//模板串的长度 int i, j, ind; for(i = ind = 0; i < len; i++) { j = str[i] - 'a';//求出字母在next中的编号 if(s[ind].next[j] == -1)//如为空则构造新的,如不为空则顺着上次的开始往下走构造 { s[sind].init();//初始化当前节点 s[ind].next[j] = sind++;//连向当前节点,并使sind加一来扩充节点 } ind = s[ind].next[j];//向下遍历 } s[ind].count++;//增加离根节点这条路径上字符串的个数,一条路上可能不止一个单词 } void make_fail()//构造失配指针 { qin = qout = 0;//初始化队列 int i, ind, ind_f; for(i = 0; i < 26; i++) { if(s[0].next[i] != -1) { q[qin++] = s[0].next[i];//先考虑根节点,和根节点相连的都入队 } } while(qin != qout) { ind = q[qout++];//记录队首节点 for(i = 0; i < 26; i++)//遍历队首节点的next { if(s[ind].next[i] != -1)//如果节点next不为空 { q[qin++] = s[ind].next[i];//将儿子节点入队 ind_f = s[ind].fail;//记录节点的失配指针指向 while(ind_f > 0 && s[ind_f].next[i] == -1)//当失配指针不为root时一直循环直到找到一个节点的儿子是i值或到了root ind_f = s[ind_f].fail; if(s[ind_f].next[i] != -1)//如果当前节点有儿子的话记录下来备用 ind_f = s[ind_f].next[i]; s[s[ind].next[i]].fail = ind_f;//使当前节点的失配指针指向刚才记录的节点完成失配指针的寻找构造。 } } } } int fd() { int ct = 0;//记录“单词的个数” int di, i, ind, p;//di为指向“文章”的指针,ind为指向失配节点的指针(即trie树中自匹配的指针)与kmp中next数组中的temp很相似 int len = strlen(des);//“文章的长度” for(di = ind = 0; di < len; di++) { i = des[di] - 'a'; while(ind > 0 && s[ind].next[i] == -1)//当ind指针不是root和找不到节点的儿子是i时一直找下去(即kmp中的while循环) ind = s[ind].fail;//一直寻找失配指针 if(s[ind].next[i] != -1)//找到了适合的失配指针 { ind = s[ind].next[i];//指向这个儿子节点,更新ind的值进行下一次匹配 p = ind;//用p来临时代替ind while(p > 0 && s[p].count != -1)//p > 0表示还没到root,count != -1表示指针前还有单词 { ct += s[p].count;//加上有的单词的个数 s[p].count = -1;//不重复计算,注意这里很重要 p = s[p].fail;//一直寻找失配指针 } } } return ct;//返回单词个数 } int main() { int cas, n; scanf("%d", &cas); while(cas-- && scanf("%d", &n)) { gets(str); cas_init();//初始化trie树 while(n-- && gets(str)) ins();//构造trie树 make_fail();//构造失配指针 gets(des); printf("%d\n", fd()); } return 0; }