【无标题】2024.3.21

2024.3.21 【少年与爱永不老去,即便披荆斩棘,丢失怒马鲜衣。】

Thursday 二月十二


AC自动机

我是oiwiki链接

前置芝士:

KMP算法

Trie (字典树)

相关例题:

P3375 【模板】KMP

P2580 于是他错误的点名开始了


AC自动机适用于字符串匹配等问题的求解,是一个Tie和KMP思想的结合体

基础的 Trie 结构,将所有的模式串构成一棵 Trie。

KMP 的思想,对 Trie 树上所有的结点构造失配指针。

不难发现,AC自动机还是比较简朴的。

失配指针是AC自动机中十分重要的一点,也是自动机构建的难点和要点,所以先看题再讲。

P3808 AC 自动机(简单版)
//2024.3.21
//by white_ice
#include<bits/stdc++.h>
using namespace std;
#define itn int
//#define fore(x) cin>>x&fo(int i=1;i<=(x);i++)


namespace triee{
const int oo = 1000006;

struct nod{
    itn fail;
    itn son[26],end;
}trie[oo];

itn cnt = 0;

void insert(string s){
    itn l = s.length();
    itn now = 0;
    for (int i=0;i<l;i++){
        if (trie[now].son[s[i]-'a']==0)
            trie[now].son[s[i]-'a'] = ++cnt;
        now = trie[now].son[s[i]-'a'];
    }
    trie[now].end+=1;
}

void get(){
    queue<int> q;
    for (itn i=0;i<26;i++){
        if (trie[0].son[i]!=0){
            trie[trie[0].son[i]].fail = 0;
            q.push(trie[0].son[i]);
        }
    }

    while (!q.empty()){
        int u = q.front();
        q.pop();
        for (itn i=0;i<26;i++){
            if (trie[u].son[i]!=0){
                trie[trie[u].son[i]].fail = trie[trie[u].fail].son[i];
                q.push(trie[u].son[i]);
            }
            else trie[u].son[i] = trie[trie[u].fail].son[i];
        }
    }
}

itn find(string s){
    int l = s.length();
    int now=0,ans=0;
    for(int i=0;i<l;++i){
        now=trie[now].son[s[i]-'a'];
        for(int t=now;t&&trie[t].end!=-1;t=trie[t].fail){
            ans+=trie[t].end;
            trie[t].end=-1;
        } 
    }
    return ans;
}
}

int n;
string s;

int main(){
    using namespace triee;
    cin >> n;

    for(itn i=1;i<=n;i++){
        cin >>s;
        triee::insert(s);
    }

    triee::trie[0].fail = 0;
    triee::get();

    cin >> s;
    cout << triee::find(s) ;

    return 0 ;
}

在本代码中使用class封装了一个完整的AC自动机,trie树的结构体中,有一个名为fail的指针,他就是失配指针。

fail指针由一个状态指向另一个状态,即状态甲是状态乙的最长后缀,注意后缀是最长的,若干个中最长的。AC自动机中,同一字符串可以匹配多个指针。

下面是fail指针的构建

考虑字典树中当前的结点 u,u 的父结点是 p,p 通过字符 c 的边指向 u,即 t r i e [ p , c ] = u trie[p,\mathtt{c}]=u trie[p,c]=u。假设深度小于 u 的所有结点的 fail 指针都已求得。

  • 如果$ \text{trie}[\text{fail}[p],\mathtt{c}] $存在:则让 u 的 fail 指针指向 trie [ fail [ p ] , c ] \text{trie}[\text{fail}[p],\mathtt{c}] trie[fail[p],c]。相当于在 p 和$ \text{fail}[p]$ 后面加一个字符 c,分别对应 u 和 f a i l [ u ] fail[u] fail[u]
  • 如果 trie [ fail [ p ] , c ] \text{trie}[\text{fail}[p],\mathtt{c}] trie[fail[p],c] 不存在:那么我们继续找到$ \text{trie}[\text{fail}[\text{fail}[p]],\mathtt{c}]$。重复 1 的判断过程,一直跳 fail 指针直到根结点。
  • 如果真的没有,就让 fail 指针指向根结点。

以上讲的还是非常清楚的啊(因为不是我讲),我只指出几个小点完善,

  1. fail指针记得要有指向目标,比如根
  2. 注意最长后缀,最长
  3. 使用时记得一定要调用初始化函数!
P3796 AC 自动机(简单版 II)
//2024.3.21
//by white_ice
#include<bits/stdc++.h>
using namespace std;
#define itn int
#define fore(x) for(int i=1;i<=x;i++)

itn n;

struct result{itn num,pos;}st[1000006];
bool operator <(result a,result b){return a.num==b.num?a.pos<b.pos:a.num>b.num;}

//namespace AC{
const int oo = 1000006;
string s[oo];

struct triee{
    itn fail;
    itn son[26],end;
}trie[oo];

int cnt = 0;

void clean(itn x){
    memset(trie[x].son,0,sizeof(trie[x].son));
    trie[x].fail =  trie[x].end = 0;
}

void init(){cnt = 0,clean(0);}

void insert(string s,itn num){
    itn l = s.length();
    itn now = 0;
    for (int i=0;i<l;i++){
        if (trie[now].son[s[i]-'a']==0){
            trie[now].son[s[i]-'a'] = ++ cnt;
            clean(cnt);
        }
        now = trie[now].son[s[i]-'a'];
    }
    trie[now].end = num;
}

void get(){
    queue<int> q;
    for(int i=0;i<26;++i){
        if(trie[0].son[i]!=0){
            trie[trie[0].son[i]].fail=0;
            q.push(trie[0].son[i]);
        }
    }
    while(!q.empty()){
        int u=q.front();
        q.pop();
        for(int i=0;i<26;++i){
            if(trie[u].son[i]!=0){
                trie[trie[u].son[i]].fail=trie[trie[u].fail].son[i]; 
                q.push(trie[u].son[i]);
            }
            else trie[u].son[i]=trie[trie[u].fail].son[i];
        }
    }
}

itn find(string s){
    itn l = s.length();
    itn now = 0,ans=  0;
    for (int i=0;i<l;i++){
        now = trie[now].son[s[i]-'a'];
        for (itn t=now;t;t=trie[t].fail)
            st[trie[t].end].num++;
    }
    return ans;
}
//}

int main(){
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    //using namespace AC;

while (cin>>n&&n){
    init();

    for (itn i=1;i<=n;i++){
        cin >> s[i];
        st[i].num = 0;
        st[i].pos = i;
        insert(s[i],i);
    }

    trie[0].fail = 0;
    get();

    cin >> s[0];

    find(s[0]);

    sort(st+1,st+n+1);
    cout << st[1].num <<endl<<s[st[1].pos]<<endl;
    for (itn i=2;i<=n;i++){
        if (st[i-1].num==st[i].num)
            cout << s[st[i].pos]<<endl;
        else break;
    }
}
    return 0;
}
  • 28
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值