AC自动机粗学

前言——不明觉厉的算法排名第一位

关于AC自动机

必要的前置知识
KMP
字典树

什么是AC自动机
AC自动机以字典树结构为基础,结合KMP的思想建立
简单来讲,建立一个AC自动机有两个步骤:
第一步: 建立字典树,将所有的模式串插入,构成一棵字典树
第二步:依据KMP的思想,对字典树上的所有结点构造失配指针

AC自动机解决什么问题?
有多个模式串的匹配问题,即多模匹配问题(在一个字符串中寻找多个模式串)

构建一个AC自动机

字典树的构建
点击这里快速回忆

失配指针的构造
AC自动机使用fail指针来帮助多模式串进行匹配
树上结点ufail指针指向另一个树上结点v,u和 v两者的关系是:v是u的最长后缀

对比AC自动机的fail指针和KMP的next指针:
相同点:是在失配时用于跳转的指针
不同点:next指针得到的是单个模式串最长的相同前后缀,fail指针指向所有模式串的前缀中匹配当前状态的最长后缀

构造fail指针的基础思想
考虑字典树中的当前结点u,u的父节点是p,p通过字符串c的边指向u,即有trie[p,c]=u。假设深度小于u的所有节点的fail指针都已求得。
第一种情况:
如果trie[fail[p],c]存在,让u的fail指针指向 trie[fail[p],c],fail[p]是p的后缀,p和fail[p]在字符串末尾一起加上一个字母c不会破坏fail[p]作为后缀的地位(前提是能够添加c)

第二种情况:
如果trie[fail[p],c]不存在(fail[p]后不存在c可供添加),那么,继续寻找trie[fail[fail[p]],c]。不断重复判断过程,直到fail指针跳到根节点

第三种情况:
到达根节点,fail指针指向根节点

转一张OI-WIKI上的图像方便理解一波
请添加图片描述
构建失配指针与自动机
使用BFS遍历字典树来进行构造
所谓的 fail 指针其实就是当前结点u对应串S的一个后缀集合

void getFail()//建立fail指针
{
    for(int i=0;i<26;i++)//0点全部指向根节点
    {
        trie[0].son[i]=1;
    }
    q.push(1);//放入根节点,开始广搜建立
    while(!q.empty())
    {
        int u = q.front();
        q.pop();
        int Fail = trie[u].fail;
        for(int i=0;i<26;i++)
        {
            int v=trie[u].son[i];
            if(!v)//v点尚未被建立,也就是字典树中没有该节点被插入
            {
                trie[u].son[i] = trie[Fail].son[i];//尝试指向u的fail指针的对应结点
                continue;
            }
            trie[v].fail = trie[Fail].son[i];//存在,直接标记fail值
            q.push(v);
        }
    }
}


多模式匹配函数query()

void query(string s)//在树上进行模式串匹配
{
    int u=1,len=s.size();
    for(int i=0;i<len;i++)
    {
        u=trie[u].son[s[i]-'a'];//因为有fail指针,可以放心遍历
        trie[u].ans++;//记录了某个前缀的出现次数
    }
}

板子

超级优化模板
使用了拓扑排序,通常情况是不需要的
some tips:
暴力跳fail的最坏时间复杂度为O(T.size()*sum_of_s.size())
经过拓扑建图优化的AC自动机的时间复杂度O(T.size())
将fail想象成有向边,如果在一个点进行操作,沿着该点的连出去的点也会进行操作

#include<bits/stdc++.h>

#define fast ios::sync_with_stdio(false), cin.tie(0), cout.tie(0)

using namespace std;

const int N = 2e6+5;
const int char_set = 26;

string s,T;
int n,cnt,vis[200051],ans,in[N],Map[N];
//为什么不用map? => 时间复杂度O(1)和O(logn)的区别

struct tr//字典树结点的结构体
{
    int son[char_set], fail, flag, ans;
    void clear()//初始化函数
    {
        memset(son,0,sizeof(son)), fail = flag = ans = 0;
    }
}trie[N];

queue<int>q;

void insert(string s,int num)//字典树插入
{
    int u=1, len=s.size();//根节点与字符串长度
    for(int i=0;i<len;i++)//遍历字符串
    {
        int v=s[i]-'a';//
        if(!trie[u].son[v])//字典树该节点尚不存在
        {
            trie[u].son[v]=++cnt;//标记该节点
        }
        u=trie[u].son[v];//向下遍历字典树
    }
    if(!trie[u].flag)//标记字典树该节点对应的字符串
    {
        trie[u].flag=num;
    }
    Map[num]=trie[u].flag;//map存字符串的尾结点位置
}

void getFail()//建立fail指针
{
    for(int i=0;i<26;i++)//0点全部指向根节点
    {
        trie[0].son[i]=1;
    }
    q.push(1);//放入根节点,开始广搜建立
    while(!q.empty())
    {
        int u = q.front();
        q.pop();
        int Fail = trie[u].fail;
        for(int i=0;i<26;i++)
        {
            int v=trie[u].son[i];
            if(!v)//v点尚未被建立,也就是字典树中没有该节点被插入
            {
                trie[u].son[i] = trie[Fail].son[i];//尝试指向u的fail指针的对应结点
                continue;
            }
            trie[v].fail = trie[Fail].son[i];//存在,直接标记fail值
            in[trie[v].fail]++;//同时v点入度加一
            q.push(v);
        }
    }
}

//通常情况不需要执行下面这个函数
void topu()//拓扑排序,避免重复遍历,入度为0的点遍历一次后不会被重复遍历
{
    for(int i=1;i<=cnt;i++)//最初,标记哪些点能够直接出去
    {
        if(in[i]==0)//入度为0,可以去掉
        {
            q.push(i);
        }
    }
    while(!q.empty())//入度为0的点不为0时
    {
        int u=q.front();//取点
        q.pop();
        vis[trie[u].flag] = trie[u].ans;//统计串的出现次数
        int v = trie[u].fail;//向上跳fail,找后缀
        in[v]--;
        trie[v].ans += trie[u].ans;//后缀加上完整串的出现次数
        if(in[v]==0)//入度为0即可准备去点
        {
            q.push(v);
        }
    }
}

void query(string s)//在树上进行模式串匹配
{
    int u=1,len=s.size();
    for(int i=0;i<len;i++)
    {
        u=trie[u].son[s[i]-'a'];//因为有fail指针,可以放心遍历
        trie[u].ans++;//记录了某个前缀的出现次数
    }
}

int main()
{
    fast;
    cin >> n;
    cnt=1;
    for(int i=1;i<=n;i++)
    {
        cin >> s;
        insert(s,i);//插入字符串
    }
    getFail();//得fail指针
    cin >> T;
    query(T);//查看模式串
    topu();//进行拓扑排序,并统计答案
    for(int i=1;i<=n;i++)
    {
        cout << vis[Map[i]] << endl;
    }
}

习题集:
P3041 [USACO12JAN]Video Game G
AC自动机与DP相结合的问题:
首先:构建一颗带fail指针的字典树
完成之后,开始进行DP,通常,状态转移方程的第一维是字符的个数第二维是字典树上结点总数,根据条件可以增加维数。
具体解法请见下方:

#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<vector>
#include<map>
#include<queue>
using namespace std;
#define ll long long
#define fast ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)

const int char_set = 3;
const int N = 3e2+5;
const int K = 1e3+5;

int cnt = 1;

int n, k;
int dp[K][N];//3e5

struct st
{
    int son[char_set], fail, num;
    void clear()
    {
        for(int i=0;i<char_set;++i)
        {
            son[i] = 0;
            fail = 0;
            num = 0;
        }
    }
}trie[N];

void insert(string s)//插入,懂得都懂
{
    int u=1, len = s.size();
    for(int i=0;i<len;++i)
    {
        int c = s[i]-'A';
        int v = trie[u].son[c];
        if(!v)
        {
            trie[u].son[c] = ++cnt;
        }
        u = trie[u].son[c];
    }
    trie[u].num++;
}

queue<int> q;
void getFail()//构建fail指针,懂得都懂
{
    for(int i=0;i<char_set;++i)
    {
        trie[0].son[i] = 1;
    }
    q.push(1);
    while(q.size()>0)
    {
        int u = q.front();
        q.pop();
        int fail = trie[u].fail;
        for(int i=0;i<char_set;++i)
        {
            int v = trie[u].son[i];
            if(!v)
            {
                trie[u].son[i] = trie[fail].son[i];
                continue;
            }
            trie[v].fail = trie[fail].son[i];
            q.push(v);
        }
        trie[u].num += trie[trie[u].fail].num;//并统计作为后缀满足的个数
    }
}

int solve()//dp部分,我滴软肋
{
    for(int i=0;i<=k;++i)
    {
        for(int j=2;j<=cnt;++j)
        {
            dp[i][j] = -1e9;
        }
    }
    for(int t=1;t<=k;++t)
    {
        for(int i=1;i<=cnt;++i)
        {
            for(int j=0;j<char_set;++j)
            {
                dp[t][trie[i].son[j]] = max(dp[t][trie[i].son[j]],dp[t-1][i]+trie[trie[i].son[j]].num);
            }
        }
    }
    int ans = 0;
    for(int i=0;i<=cnt;++i)
    {
        ans = max(ans,dp[k][i]);
    }
    return ans;
}

int main()
{
    fast;
    cin >> n >> k;
    for(int i=1;i<=n;++i)
    {
        string s;
        cin >> s;
        insert(s);
    }
    getFail();
    cout << solve() << endl;
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值