hdu2222Keywords Search,caioj1464(AC自动机模板)

本文详细介绍AC自动机原理及其应用,包括字典树、失败指针的概念,并通过具体代码实例展示了如何构建AC自动机以解决字符串匹配问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目传送门
题意:
有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自动机真的牛逼!!失败指针真的伟大!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值