题目传送门
题意:
有n(n<=10000)个单词(长度不超过50,保证都为小写字母)和一句话(长度不超过1000000)
求出这句话包括几个单词 。
注意:一个单词重复出现,只算作出现了一个单词,如果有多个重复的单词,那么重复单词应该计算多次
比如有两个单词,ab,ab(可以重复)
那么如果句子里有ab这个单词,那么出现了两个单词。
如果只有一个单词是ab。
那么句子里有abab这个东西的话只算做出现一个单词。
解法:
AC自动机模版。
这里算是一个详细的模版介绍了。
没有学过字典树的同学先学字典树。
AC自动机的原理就是字典树+失败指针。
字典树的每一个节点都有一个失败指针。
比如:
i的失败指针为j。j不能等于i
表示的是:
root到j所成的字符串是
root到i所成的字符串的后缀。
而且是最长的。
跟KMP的原理是一样的。
如果我找到一个字符结果不匹配我下一个字符的话,那么我就跳到失败指针。
那么如何求失败指针呢?
比如说我现在要求i的失败指针,也就是我们要在字典树上面求i的后缀。
那么我们找到i的父亲节点。记为f。
那么我们去跳f的失败指针。记为ff
那么root与ff所构成的字符串一定是root与f所构成的字符串的后缀(这是定义啊!!!)
而且是最长的。
如果ff有一个孩子为i这个字符。记为x
那么i的失败指针就为x。
因为f是i的前一个字符。
如果f的后缀的后一个字符与i匹配的话。
那也一定是i的后缀。
因为到f已经是最长的了。那么到i也是最长的。
注意这里要用宽搜实现,不能用深搜来实现。
因为深搜的话搜完一条链可能要用到其他的链,而这个时候可能其他的链还没有搜过。
所以要用宽搜来一层一层做
代码实现:(如何求失败指针 )
queue<int> q;
void bfs() {
int x;
q.push(0);
while(q.empty()==0) {
x=q.front();
for(int i=1;i<=26;i++) { //我们现在处理的是x的孩子的失败指针
int son=t[x].c[i];
if(son==-1) //如果没有这个孩子就下一个
continue;
if(x==0)
t[son].fail=0; //x=0表示x为root,所以没有后缀,所以失败指针为0
else {
int j=t[x].fail; //fail即为失败指针。
while(j!=0&&t[j].c[i]==-1) //找到能构成son的后缀的人
j=t[j].fail;
t[son].fail=max(t[j].c[i],0);//有可能while跳出来之后还找不到有人能构成后缀的话,fail就为0.指向root。
}
q.push(son);
}
q.pop();
}
}
求到了失败指针的话那么怎么来求解呢。
我们在每个字符串的末尾节点记录s。
s表示一这个节点为结尾的有多少个串。
比如:
abcd这个串。
d为结尾。
那么d这个节点s就++;
那么当我们问到一个节点就加上他的s,然后把他的s变为-1。
表示不可用,加过答案就不能再加了。
还是拿abcd来说。
当我们问到d这个节点的时候,加上他的s就把他的s变成-1。
因为一个字符串只能算作出现一次。
加过答案一定要变成-1。
求答案:
一开始x=root
问那句话的每一个字母。
如果x的孩子里有当前这个位置的字母的话。
那么x就等于他的这个孩子。
然后我们每问一个节点都要加上他的s,而且还要加上他的失败指针的s。
j=x;
while(t[j].s>=0) { //每次跳失败指针。
ans+=t[j].s;
t[j].s=-1;
j=t[j].fail;
}
为什么每问一个节点都要加上他的s呢???
而且还要跳失败指针???
因为:
当前节点有可能还不是某一个单词的结尾。
但是有些其他的单词有可能作为以他为后缀出现,那么我们就要跳失败指针看一下有没有这个单词。
可能有点难理解。
比如:
有两个串。
abcd
bc
那么我们当前问到的是abcd里面的c节点。
很显然c在这个串中不是作为结尾的。
那么他的c=0。
但是,我们还有一个叫做bc的单词啊!
这就是为什么每一个节点都要去跳失败指针了。
这样的话c一跳失败指针就跳到bc里面的c了。
这样的话bc串也成功得记录到答案里了。
#include<queue>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cstdlib>
#include<cmath>
using namespace std;
struct node {
int s,c[27],fail;
node() {
s=fail=0; //s就表示以这个节点为结尾的字符串有多少个
memset(c,-1,sizeof(c));
}
} t[510000];
int tot,ans,n;
char a[1100000];
void clean(int x) {
t[x].s=t[x].fail=0;
for(int i=1;i<=26;i++)
t[x].c[i]=-1;
}
void bt(int root) {
int x=root,len=strlen(a+1);
for(int i=1;i<=len;i++) {
int y=a[i]-'a'+1;
if(t[x].c[y]==-1) {
t[x].c[y]=++tot;
clean(tot);
}
x=t[x].c[y];
}
t[x].s++;
}
queue<int> q;
void bfs() { //求出每个节点的失败指针
int x;
q.push(0);
while(q.empty()==0) {
x=q.front();
for(int i=1;i<=26;i++) {
int son=t[x].c[i];
if(son==-1)
continue;
if(x==0)
t[son].fail=0;
else {
int j=t[x].fail;
while(j!=0&&t[j].c[i]==-1)
j=t[j].fail;
t[son].fail=max(t[j].c[i],0);
}
q.push(son);
}
q.pop();
}
}
void solve() {
int x=0,len=strlen(a+1),j;
for(int i=1;i<=len;i++) {
int y=a[i]-'a'+1;
while(x!=0&&t[x].c[y]==-1) //跳失败指针直到有这个孩子了
x=t[x].fail;
x=t[x].c[y];
if(x==-1) {//如果跳完还是没有这个孩子那么下一个,然后x=0重新寻找
x=0;
continue;
}
j=x;
while(t[j].s>=0) { //跳
ans+=t[j].s;
t[j].s=-1;
j=t[j].fail;
}
}
printf("%d\n",ans);
}
int main() {
int T;
scanf("%d",&T);
while(T--) {
ans=tot=0;
scanf("%d",&n);
clean(0);
for(int i=1; i<=n; i++) {
scanf("%s",a+1);
bt(0);
}
bfs();
scanf("%s",a+1);
solve();
}
return 0;
}
太强了!!orz!!
AC自动机真的牛逼!!失败指针真的伟大!!