AcWing 算法提高课 AC自动机

AC自动机=Trie+kmp

优化:Trie图

1、kmp

长字符串s和模板串p都以下标1开始。

(1) 求next数组:kmp的next数组存的是p的自匹配,即以p[i]为结尾的后缀能够匹配的最长非平凡(不是自身)前缀。由于非平凡,next[0]=next[1]=0,循环从2开始。

(2)进行匹配,将模板串p在长字符串s上移动,并求出当前可以匹配的最长长度,如果此长度为p的长度,则匹配成功。循环从1开始(可能p长度为1并且直接匹配成功 )

2、AC自动机模板:

AC自动机可以解决和多个模板串匹配的问题,用BFS在trie上建立ne数组,匹配过程和kmp类似。

例题:https://www.acwing.com/blog/content/404/

模板:统计模板串在长字符串中出现的个数

int n;
const int N=10010;
const int S=55,M=1000010;

int tr[N*S][26];//自动机数组,第一维节点,第二维子节点
int cnt[N*S];//以当前节点结尾的单词数量
int idx;

char str[M];
int que[N*S];
int ne[N*S];

void Insert()
{
    int p=0;
    for(int i=0;str[i];i++)
    {
        int t=str[i]-'a';
        if(!tr[p][t]) tr[p][t]=++idx;
        p=tr[p][t];
    }
    cnt[p]++;
}

void Build()
{
    int hh=0,tt=-1;
    
    for(int i=0;i<26;i++)
    {
        if(tr[0][i])
            que[++tt]=tr[0][i];
    }
    while(hh<=tt)
    {
        int t=que[hh++];
        for(int i=0;i<26;i++)
        {
            int c=tr[t][i];
            if(!c) continue;
            int j=ne[t];
            while(j&&!tr[j][i]) j=ne[j];
            if(tr[j][i]) j=tr[j][i];
            ne[c]=j;
            que[++tt]=c;
        }
    }
}
void YD()
{
    memset(tr,0,sizeof(tr));
    memset(cnt,0,sizeof(cnt));
    memset(ne,0,sizeof(ne));
    idx=0;
    cin>>n;
    while(n--)
    {
        cin>>(str);
        Insert();//trie插入
    }
    Build();//构建ac自动机
    
    cin>>str;
    int res=0;
    for(int i=0,j=0;str[i];i++)
    {
        int t=str[i]-'a';
        while(j&&!tr[j][t]) j=ne[j];
        if(tr[j][t]) j=tr[j][t];
        //统计答案不光要统计j 还要统计更短的可能出现的单词,即往ne[j]去查询
        
        int p=j;
        while(p)
        {
            res+=cnt[p];
            cnt[p]=0;
            p=ne[p];
        }
        
    }
    cout<<res<<endl;
}
 
View Code

 3、Trie图

相当于把AC自动机路径压缩,注意和2中模板里,build函数和统计过程中的不同。

代码更短,常数更小。

例题:https://www.acwing.com/blog/content/404/

模板:统计模板串在长字符串中出现的个数

#include<bits/stdc++.h>

#define fore(x,y,z) for(LL x=(y);x<=(z);x++)
#define forn(x,y,z) for(LL x=(y);x<(z);x++)
#define rofe(x,y,z) for(LL x=(y);x>=(z);x--)
#define rofn(x,y,z) for(LL x=(y);x>(z);x--)
#define pub push_back
#define all(x) (x).begin(),(x).end()
#define fi first
#define se second

using namespace std;
typedef long long LL;
typedef pair<int,int> PII;
typedef pair<LL,LL> PLL;
int n;
const int N=10010;
const int S=55,M=1000010;

int tr[N*S][26];//自动机数组,第一维节点,第二维子节点
int cnt[N*S];//以当前节点结尾的单词数量
int idx;

char str[M];
int que[N*S];
int ne[N*S];

void Insert()
{
    int p=0;
    for(int i=0;str[i];i++)
    {
        int t=str[i]-'a';
        if(!tr[p][t]) tr[p][t]=++idx;
        p=tr[p][t];
    }
    cnt[p]++;
}

void Build()
{
    int hh=0,tt=-1;
    
    for(int i=0;i<26;i++)
    {
        if(tr[0][i])
            que[++tt]=tr[0][i];
    }
    while(hh<=tt)
    {
        int t=que[hh++];
        for(int i=0;i<26;i++)
        {
            int c=tr[t][i];
            if(!c) tr[t][i]=tr[ne[t]][i];
            //如果没有这个儿子,则直接把tr指向上一个匹配位置
            else
            {
                ne[c]=tr[ne[t]][i];
                //如果上一个有则直接匹配,如果没有,则已经被压缩
                que[++tt]=c;
            }

        }
    }
}
void YD()
{
    memset(tr,0,sizeof(tr));
    memset(cnt,0,sizeof(cnt));
    memset(ne,0,sizeof(ne));
    idx=0;
    cin>>n;
    while(n--)
    {
        cin>>(str);
        Insert();//trie插入
    }
    Build();//构建ac自动机
    
    cin>>str;
    int res=0;
    for(int i=0,j=0;str[i];i++)
    {
        int t=str[i]-'a';
        j=tr[j][t];

        int p=j;
        while(p)
        {
            res+=cnt[p];
            cnt[p]=0;
            p=ne[p];
        }
        
    }
    cout<<res<<endl;
}
 
int main()
{
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);
    int T=1;
    cin >> T;
    while (T--)
    {
        YD();
    }
    return 0;
}
View Code

 

4、例题

AC自动机上dp:dp[i][j]代表移动i位且匹配到trie的j位置时的最小修改次数

注意bad标识了坏前缀

https://www.acwing.com/problem/content/1055/

 

Trie图+dp+拓扑排序:统计文章中全部单词各自的出现次数(作为其他单词的子串也算)

https://www.acwing.com/problem/content/1287/

此题的思想是,将单词出现,转化为单词作为全部前缀的后缀出现,对应ne的定义(前缀的后缀)。

cnt统计的就是单词出现次数,插入时统计的是作为平凡(即后缀就是本身)后缀的前缀出现的次数,拓扑排序累加的过程是累加作为非平凡后缀的前缀出现的次数,累加后就是作为全部后缀的前缀出现的次数。

 

posted @ 2022-09-29 17:39  80k  阅读(65)  评论(0编辑  收藏  举报