AC自动机

AC 自动机是 以 Trie 的结构为基础 ,结合 KMP 的思想 建立的。
@[toc]

解决:多模式匹配问题

匹配问题:
有多个不同的模式串s[1...N]
给定长度=M的目标串t,求每个模式串s[i]总共在t中出现了多少次
跑N遍kmp

字典树:

对所有的模式串s[1...N]构建trie
字典树的根到字典树上任意节点构成的路径都对应某个模式串s[i]的前缀
概念:
状态:对字典树的任何一个结点都看做状态,对应某个模式串s[i]的前缀
转移:任意一个状态添加一个字符得到新状态
可识别:字符串能对应上字典树上的一个状态
在这里插入图片描述
"the"为可识别状态

int tot=∑|s|;
struct Node{
    int id;//s[id]结尾 
    Node *nxt[26];
}pool[tot],*rt;
void insert(string s,int id){
    Node *p=rt;
    int c;
    for(int i=0;i<s.length();i++)
    {
        c=s[i]-'0';
        if(p->nxt[c]==NULL)p->next[c]=pool+(++top);
        p=p->nxt[c];
        if(i==s.length()-1)p->id=id;
    }
}

Fail指针

AC自动机利用一个fail指针来辅助多模式串的匹配
状态u的fail指针指向另一个可识别状态v,且v是u的最长后缀,即fail[u] = 状态u的最长可识别后缀
在这里插入图片描述

求fail[u]指针

图中红色线为fail指针所指
在这里插入图片描述
所谓border,就是next[len(β)],直观含义是除了串本身以外,使得前缀等于后缀的最长的一段前缀。
类比求next[j] = pre(s,j)的最长border
pre(s,j-1)的所有border长度为k1 = next[j-1], k2=next[next[j-1]]....
需要找到最大的k使得s[k+1] = s[j] 成立了
此时next[j] = k+1

现在我们来看fail,字典树当前的状态为p,p添加字符c的边(记为p->c)指向状态u,即p->c=u。并假设深度小于u的所以结点的fail指针都已求得
p的所有可识别后缀:k1=fail[p], k2=fail[fail[p]],.....
找到其中深度最大的状态k使得k->c 可识别(存在)
此时fail[u]= k->c

如果fail[p]->c存在:则让fail[u]=fail[p]->c.相当于在p和fail[p]后面加一个字符c,分别对应u和fail[u]
如果fail[p]->c不存在:那么继续检查fail[fail[p]]->c,一直跳fail指针直到根节点,如果真没有,那就fail[u]=rt,指向根节点

代码:

void bulid()
{
    queue<Node*>q;
    rt->fail=rt;
    for(int k=0;k<26;k++)
    {
        if(rt->nxt[k])q.push(rt->nxt[k]);
        else rt->nxt[k]=rt;
        rt->nxt[k]->fail=rt;
    }
    //bfs
    while(!q.empty())
    {
        Node *p=q.front();
        q.pop();
        for(int k=0;k<26;k++)
        {
            if(p->nxt[k])//如果有孩子 
            {
                p->nxt[k]->fail=p->fail->nxt[k];
                q.push(p->nxt[k]);
            }
            else p->nxt[k]=p->fail->nxt[k];//如果没有孩子,(相当于路径压缩) ,图中的黑线部分
        }
    }
}
void build() {
  for (int i = 0; i < 26; i++)
    if (tr[0][i]) q.push(tr[0][i]);
  while (q.size()) {
    int u = q.front();
    q.pop();
    for (int i = 0; i < 26; i++) {
      if (tr[u][i])
        fail[tr[u][i]] = tr[fail[u]][i], q.push(tr[u][i]);
      else
        tr[u][i] = tr[fail[u]][i];
    }
  }
}

在这里插入图片描述
最终形态:

在这里插入图片描述

多模式匹配

  1. 建trie O(∑|S|)
  2. 求所有状态的fail指针
    fail已经没有空指针,所以在图上大胆走
    在这里插入图片描述
    黄色为模式串
    问模式串在t中出现几次?
    直接计算模式串上有几个i不可,虽然01,010,11都是正确的(01上i出现了四次,i=2,4,5,8),但是1却不对,1上一个i也没有,但是1在t中五次
    暴力向上跳fail,一直跳到rt,该路径上的串均为出现
    到01时,跳fail到1,又到rt,01和1均出现一次
    也就是,fail链上所有模式串都加一
    在这里插入图片描述

    代码:

int query(char *t) {
  int u = 0, res = 0;
  for (int i = 1; t[i]; i++) {
    u = tr[u][t[i] - 'a'];  // 转移
    for (int j = u; j && e[j] != -1; j = fail[j]) {
      res += e[j], e[j] = -1;
    }
  }
  return res;
}

但是!!复杂度太高了
会被卡,
正确做法是建fail树
每个点u认fail[u]为爸爸(有点像并查集的形式)
(黄色为模式串)
在这里插入图片描述
向上fail的路径上都加一

我们能发现下列特征:
1.u->rt : 链+1
2.单点求值
3.离线操作
可以用树上差分,复杂度 O(节点个数∑|S|)
全部加完的fail树
在这里插入图片描述
模式串1的出现次数 = 子树内所有1求和 = 5
AC自动机:总复杂度:O(∑|S|+26*∑|S|+|t|)

总代码:

#include<bits/stdc++.h>
#define MAXN 200005
using namespace std;
int N,Q;
struct Node{
    int cnt;
    Node *nxt[26];
    Node* fail;
    vector<Node*>adj;
}*rt; 
int top=0;
Node pool[MAXN];
Node* pos[MAXN];
void insert(string s,int x){
    Node *p=rt;
    int id;
    for(int i=0;i<s.length();i++)
    {
        id=s[i]-'a';
        if(p->nxt[id]==NULL)p->nxt[id]=pool+(++top);
        p=p->nxt[id];
        if(i==s.length()-1)pos[x]=p;
    }
}
void bulid()
{
    queue<Node*>q;
    rt->fail=rt;
    for(int k=0;k<26;k++)
    {
        if(rt->nxt[k])
        {
            rt->adj.push_back(rt->nxt[k]);
            q.push(rt->nxt[k]);
        }
        else rt->nxt[k]=rt;

        rt->nxt[k]->fail=rt;
    }

    while(!q.empty())
    {
        Node *p=q.front();
        q.pop();
        for(int k=0;k<26;k++)
        {
            if(p->nxt[k])//如果有孩子 
            {
                p->nxt[k]->fail=p->fail->nxt[k];
                p->fail->nxt[k]->adj.push_back(p->nxt[k]);
                q.push(p->nxt[k]);
            }
            else p->nxt[k]=p->fail->nxt[k];//如果没有孩子,(相当于路径压缩) ,图中的黑线部分
        }
    }
}
string s,t;
void dfs(Node* p)
{
    for(int k=0;k<p->adj.size();k++)
    {
        dfs(p->adj[k]);
        p->cnt+=p->adj[k]->cnt;
    }
}
int main()
{
    ios::sync_with_stdio(false);
    cin>>N;
    rt=pool;
    for(int i=1;i<=N;i++)
    {
        cin>>s;
        insert(s,i);
    }
    bulid();
    cin>>t;
    Node *p=rt;
    for(int i=0;i<t.length();i++)
    {
        p=p->nxt[t[i]-'a'];
        ++(p->cnt);
    }
    dfs(rt);
    for(int i=1;i<=N;i++)
    {
        cout<<pos[i]->cnt<<endl;
    }
    return 0;
}
全部评论

相关推荐

鼠鼠第一次实习,啥也不懂一直是自己一个人吃的饭,不会做工作老是被嫌弃,大人的世界是这样的吗?
我是星星我会发亮:好的mt有两种,一种愿意教你的,一种几乎什么活都不给你派让你很闲允许你做自己事情的
实习吐槽大会
点赞 评论 收藏
分享
点赞 评论 收藏
分享
点赞 评论 收藏
分享
避坑恶心到我了大家好,今天我想跟大家聊聊我在成都千子成智能科技有限公司(以下简称千子成)的求职经历,希望能给大家一些参考。千子成的母公司是“同创主悦”,主要经营各种产品,比如菜刀、POS机、电话卡等等。听起来是不是有点像地推销售公司?没错,就是那种类型的公司。我当时刚毕业,急需一份临时工作,所以在BOSS上看到了千子成的招聘信息。他们承诺无责底薪5000元,还包住宿,这吸引了我。面试的时候,HR也说了同样的话,感觉挺靠谱的。于是,我满怀期待地等待结果。结果出来后,我通过了面试,第二天就收到了试岗通知。试岗的内容就是地推销售,公司划定一个区域,然后你就得见人就问,问店铺、问路人,一直问到他们有意向为止。如果他们有兴趣,你就得摇同事帮忙推动,促进成交。说说一天的工作安排吧。工作时间是从早上8:30到晚上18:30。早上7点有人叫你起床,收拾后去公司,然后唱歌跳舞(销售公司都这样),7:55早课(类似宣誓),8:05同事间联系销售话术,8:15分享销售技巧,8:30经理训话。9:20左右从公司下市场,公交、地铁、自行车自费。到了市场大概10点左右,开始地推工作。中午吃饭时间大约是12:00,公司附近的路边盖饭面馆店自费AA,吃饭时间大约40分钟左右。吃完饭后继续地推工作,没有所谓的固定中午午休时间。下午6点下班后返回公司,不能直接下班,需要与同事交流话术,经理讲话洗脑。正常情况下9点下班。整个上班的一天中,早上到公司就是站着的,到晚上下班前都是站着。每天步数2万步以上。公司员工没有自己的工位,百来号人挤在一个20平方米的空间里听经理洗脑。白天就在市场上奔波,公司的投入成本几乎只有租金和工资,没有中央空调。早上2小时,晚上加班2小时,纯蒸桑拿。没有任何福利,节假日也没有3倍工资之类的。偶尔会有冲的酸梅汤和西瓜什么的。公司的晋升路径也很有意思:新人—组长—领队—主管—副经理—经理。要求是业绩和团队人数,类似传销模式,把人留下来。新人不能加微信、不能吐槽公司、不能有负面情绪、不能谈恋爱、不能说累。在公司没有任何坐的地方,不能依墙而坐。早上吃早饭在公司外面的安全通道,未到上班时间还会让你吃快些不能磨蹭。总之就是想榨干你。复试的时候,带你的师傅会给你营造一个钱多事少离家近的工作氛围,吹嘘工资有多高、还能吹自己毕业于好大学。然后让你早点来公司、无偿加班、抓住你可能不会走的心思进一步压榨你。总之,大家在找工作的时候一定要擦亮眼睛,避免踩坑!———来自网友
qq乃乃好喝到咩噗茶:不要做没有专业门槛的工作
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务