题目链接 : http://acm.hdu.edu.cn/showproblem.php?pid=3065
题意:给出几个字符串,问这些字符串在主字符串中出现了几次;
思路:
做这道题之前发现对AC自动机的fail指针有一点误解,一开始的时候,我以为fail指针只会指向相邻的Trie树的一个枝干上的某个字符,不会指向自己的这一条枝干上的,什么意思呢? 比如两个字符串 dacd cdc 显然,这两个字符串如果建立Trie树的时候,从第一个字符开始就已经分支了, 我最开始对fail的理解就是,d的fail指针只会指向第二个子字符串的 d,但事实上,fail最先指向的应该是第一个子字符串的第一个 d,因为存在这个误解,所以这道题卡了一段时间;如果fail指针不会指向自己这条Trie树枝干的话,就可能会有遗漏的可能,如果问题是查找一个子字符串在主字符串中出现过几次;假设主字符串是 acdacdacd, 当我们匹配到主字符串中第二个d的时候,就已经到第一个子字符串的末尾了,这时候就应该去访问第一个子字符串中最后一个d所指向的那个fail,假设指向的是第二个子字符串的d,如果还是匹配失败,又会继续访问第二个字符串的d的fail,显然这时候的fail指向的是root(根节点),然后就结束匹配了,但事实上,从主字符串中,第二个d的后面还有一个字符串可以匹配成功(dac d acd),这就是遗漏的情况;实际上的fail是如何运作的呢?
假设主字符串是 AAACBBGCF; 子字符串是 AA; AAC ; GCF CF ; (上图中边框为红色表示是一个子字符串的末尾,当匹配到这个地方的时候,个数 + 1)
用p来表示正在匹配的字符在主字符串的位置,从 1开始 ;p = 1;
p = 1的时候,Trie从root开始的,匹配第一个字符,root开始去看它的子节点有没有和p = 1的字符串对应的,发现左边第一行的A能和 p = 1的字符匹配,但并不是一个子字符串的末尾,然后开始访问 root的fail (fail的访问默认到root就结束的),访问结束后Trie中匹配的位置就是第一行的A这个位置,这时候p++,即p = 2,然后第一行的A开始去看它的子节点有没有和p = 2这个字符匹配的,显然是有的,而且是一个子字符串的末尾,所以个数+1,然后开始访问第一行的A的fail指针(按照一般情况,应该去查看这个fail指针指向的节点的子节点是否和当前匹配的字符为同一个字符,如果是而且是一个子字符串的末尾,那么个数+1,不管是不是末尾也要继续访问当前节点的fail,但是这里fail是root,模版的访问限制是到root就结束);访问结束过后,p++,p = 3;然后第二个A开始去看有没有和p = 3的字符串匹配的,发现并没有,然后这个时候,开始访问第二个A的fail,(这里要注意的是,不管当前节点的子节点是否能和主字符串中对应的位置的字符相匹配,也要照常访问当前这个节点的fail),第二个A的fail指向第一个A,这个A的子节点(也就是第二个A)表示的字符正好和p = 3的字符相匹配,而且是一个子字符串的末尾,这个时候+1,然后继续从第一个A开始访问fail,访问结束过后,p++,p = 4,然后开始去看第二个A的子节点有没有和p = 4匹配的,显然是有的,而且是一个子字符串的末尾,这个时候个数+1,然后仍然开始访问fail,访问结束和发现当前的Trie已经访问到底部了,就重新倒回到root,继续向下找,当p = 7的时候,才发现root的子节点有能和p = 7匹配的,就是中间的G,之后的过程和上面的过程差不多,我就直接跳到p = 9的时候,p = 9的时候,Trie匹配的位置是第三行的F,是一个子字符串的末尾,个数+1,然后开始访问C的fail,发现C的fail所指向的子节点也是F,并且也是一个子字符串的末尾,然后个数+1;差不多就这么一个过程,匹配的例子包含了两种,一种是 AAA匹配子字符串AA的情况,和GCF匹配子字符串GCF,CF的情况;这些弄懂了的话,这道题就能做了;下面给出代码(是指针版的,个人比较喜欢指针,数组的模版我看不懂) (我这里提醒一下,我是为了让你们比较好理解,图片上面那部分的文字说明和图片下面的是有点出入的,但是以图片下面的为准,上面的如果发现有什么不对的可以直接忽视掉,看图片下面的文字说明)
#include<iostream>
#include<string>
#include<cstring>
#include<cstdio>
#include<queue>
using namespace std;
#define Maxn 2000005
char ptr[1005][55],s[Maxn];
int vis[1005]; // 记录编号的个数
struct Trie {
int Ed;
Trie *child[95];
Trie *fail;
Trie () {
memset(child,0,sizeof(child));
fail = NULL;
Ed = 0;
}
} *root;
void insert_ (char *str,int x) {
int index, len = strlen(str);
Trie *rt = root;
for (int i = 0; i < len; ++i) {
index = str[i];
if(!rt->child[index]) rt->child[index] = new Trie ();
rt = rt->child[index];
}
rt->Ed = x; // 在子字符串的末尾标记编号
}
void getfail () {
Trie *rt = root,*p;
queue<Trie *> qu;
qu.push(rt);
while (!qu.empty()) {
rt = qu.front(); qu.pop();
for (int i = 0; i < 95; ++i) {
if(rt->child[i]) {
if(rt == root) rt->child[i]->fail = root;
else {
p = rt->fail;
while (p) {
if(p->child[i]) {
rt->child[i]->fail = p->child[i];
break;
}
else p = p->fail;
}
if(!p) rt->child[i]->fail = root;
}
qu.push(rt->child[i]);
}
}
}
}
void query (char *str) {
int index, len = strlen(str);
Trie *rt = root, *p;
for (int i = 0; i < len; ++i) {
index = str[i];
if(index >= 95) continue;
while (!rt->child[index] && rt != root) rt = rt->fail;
rt = rt->child[index];
rt = (rt == NULL) ? root : rt;
p = rt;
while (p != root && p->Ed >= 0) {
vis[p->Ed]++; // 遇到编号就++
p = p->fail;
}
}
}
void refree (Trie *rt) {
for (int i = 0; i < 95; ++i) {
if(rt->child[i]) refree(rt->child[i]);
delete rt->child[i];
}
}
int main (void)
{
int n;
while (scanf("%d",&n) != EOF) {
root = new Trie();
for (int i = 1; i <= n; ++i) { scanf(" %s",ptr[i]); insert_ (ptr[i],i); }
memset(vis,0,sizeof(vis));
getchar();
gets(s);
getfail();
query (s);
for (int i = 1; i <= n; ++i) {
if(vis[i] > 0) {
printf("%s: %d\n",ptr[i],vis[i]);
}
}
refree(root);
}
return 0;
}